Warm-up · Activity 1 of 7
Warm-up: a project’s package.json contains the script below. Which command runs it from the project folder?
{
"scripts": {
"start": "node src/server.js"
}
}// B1.1 · ~32 min · Beginner
After this lesson you can set up a small Node.js project that runs in the module system you intended, reads its configuration from a .env file without type bugs, and fails with a clear message and a non-zero exit code.
Lesson 1 of 5 in B1 Running Node and project basics
You will be able to
Warm-up · Activity 1 of 7
{
"scripts": {
"start": "node src/server.js"
}
}Predict · Activity 2 of 7
// .env
// PORT=3000
// DEBUG=false
// app.mjs
const port = process.env.PORT;
console.log(port + 1);
console.log(process.env.DEBUG ? 'debug on' : 'debug off');Practice · Activity 3 of 7
Practice · Activity 4 of 7
// index.js
const fs = require('node:fs');
console.log(typeof fs.readFileSync);Practice · Activity 5 of 7
Brain teaser · Activity 6 of 7
// teaser.mjs
import { readFile } from 'node:fs/promises';
async function loadConfig() {
try {
return readFile('missing.json', 'utf8');
} catch {
return '{}';
}
}
console.log(await loadConfig());Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
This program loads .env itself with process.loadEnvFile, which does what node --env-file=.env main.js would do, so it runs with plain node main.js. The first line shows the trap: PORT arrives as a string, so + 1 appends, and the string "false" is truthy. Then it converts each value once, at the edge of the program, and reads an optional features.json with the await inside try, so the missing file lands in catch and the program falls back to {}.
main.js
import {loadEnvFile} from 'node:process';
import {readFile} from 'node:fs/promises';
// Does what node --env-file=.env main.js does: each line becomes a string in process.env.
loadEnvFile('.env');
console.log(typeof process.env.PORT, process.env.PORT + 1, Boolean(process.env.DEBUG));
// Convert once, at the edge of the program.
const port = Number(process.env.PORT);
const debug = process.env.DEBUG === 'true';
console.log({port, debug});
// Await inside try, so a missing file lands in catch.
let features = {};
try {
features = JSON.parse(await readFile('features.json', 'utf8'));
} catch (error) {
if (error.code !== 'ENOENT') throw error;
console.log('features.json:', error.code, '- using {}');
}
console.log({port, debug, features});
.env
PORT=8080
DEBUG=false
Run it with
node main.jsOutput
string 80801 true
{ port: 8080, debug: false }
features.json: ENOENT - using {}
{ port: 8080, debug: false, features: {} }Exercise 1 of 2
Write readConfig(env). env is an object of strings, as process.env is after node --env-file=.env. Return {port, debug}: port is PORT as a number, 3000 when PORT is not set; debug is true only when DEBUG is exactly "true". Throw an Error when PORT is not a whole number from 1 to 65535. In a real program you call readConfig(process.env); the tests pass plain objects instead, so this runs in the browser too.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
env.PORT ?? '3000' uses 3000 only when PORT is not set at all.
Number("abc") is NaN, which is not an integer: Number.isInteger(port) catches it.
Compare the string: env.DEBUG === 'true'. Anything else, including "false", gives false.
One way to solve it. Yours can look different and still pass the checks.
// env is an object of strings, like process.env.
export function readConfig(env) {
const port = Number(env.PORT ?? '3000');
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error('PORT must be a whole number from 1 to 65535, got "' + env.PORT + '"');
}
return {port, debug: env.DEBUG === 'true'};
}
console.log(readConfig({PORT: '8080', DEBUG: 'false'}));
Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// env is an object of strings, like process.env.
export function readConfig(env) {
return {port: env.PORT, debug: env.DEBUG};
}
console.log(readConfig({PORT: '8080', DEBUG: 'false'}));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readConfig} from './main.js';
test('PORT="8080" becomes the number 8080', () => {
const got = readConfig({PORT: '8080'}).port;
assert.equal(got, 8080, `port was ${JSON.stringify(got)}`);
});
test('DEBUG="false" gives false and DEBUG="true" gives true', () => {
assert.equal(readConfig({DEBUG: 'false'}).debug, false, 'DEBUG="false" should give the boolean false');
assert.equal(readConfig({DEBUG: 'true'}).debug, true, 'DEBUG="true" should give the boolean true');
});
test('no PORT gives port 3000', () => {
const got = readConfig({}).port;
assert.equal(got, 3000, `port was ${JSON.stringify(got)}`);
});
test('PORT="abc" or "70000" throws an Error', () => {
assert.throws(() => readConfig({PORT: 'abc'}), Error, 'PORT="abc" should throw');
assert.throws(() => readConfig({PORT: '70000'}), Error, 'PORT="70000" should throw');
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 2
Write loadJson(path, fallback) with readFile from node:fs/promises. It returns the parsed JSON in the file. When the file does not exist (error.code is ENOENT), it returns fallback instead. Any other error, such as broken JSON, must still be thrown. The last lines of main.js already catch that error, print it and set process.exitCode = 1. Run it with node main.js and check it with node --test.
This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.
Put only the readFile call in try, with await, so its rejection lands in your catch.
In catch, check error.code === 'ENOENT' and return fallback; otherwise throw error again.
Parse after the try: then a SyntaxError from JSON.parse is not mistaken for a missing file.
One way to solve it. Yours can look different and still pass the checks.
import {readFile} from 'node:fs/promises';
// The parsed JSON in the file, or fallback when the file does not exist.
export async function loadJson(path, fallback) {
let text;
try {
text = await readFile(path, 'utf8');
} catch (error) {
if (error.code === 'ENOENT') return fallback;
throw error;
}
return JSON.parse(text);
}
try {
console.log(await loadJson('settings.json', {}));
} catch (error) {
console.error('Cannot read settings:', error.message);
process.exitCode = 1;
}
Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
import {readFile} from 'node:fs/promises';
// The parsed JSON in the file, or fallback when the file does not exist.
export async function loadJson(path, fallback) {
const text = await readFile(path, 'utf8');
return JSON.parse(text);
}
try {
console.log(await loadJson('settings.json', {}));
} catch (error) {
console.error('Cannot read settings:', error.message);
process.exitCode = 1;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadJson} from './main.js';
test('reads and parses settings.json', async () => {
const got = await loadJson('settings.json', {});
assert.deepEqual(got, {theme: 'dark'}, `loadJson("settings.json") returned ${JSON.stringify(got)}`);
});
test('a missing file gives the fallback', async () => {
const got = await loadJson('missing.json', {theme: 'light'});
assert.deepEqual(got, {theme: 'light'}, `loadJson("missing.json", fallback) returned ${JSON.stringify(got)}`);
});
test('broken JSON still throws a SyntaxError', async () => {
await assert.rejects(loadJson('broken.json', {}), SyntaxError, 'loadJson("broken.json") should reject with a SyntaxError, not return the fallback');
});
settings.json
{"theme": "dark"}
broken.json
{"theme": }
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsconst fs = require('node:fs');
console.log(typeof fs.readFileSync);
What Node.js prints
ReferenceError: require is not defined in ES module scope, you can use import insteadWhy, and the fix
The project's package.json says "type": "module", so main.js is an ES module, and ES modules have no require. Write import fs from 'node:fs'; instead, or, for one file that must stay CommonJS, rename it to .cjs.
import {join} from 'node:path';
console.log(join(__dirname, 'config.json'));
What Node.js prints
ReferenceError: __dirname is not defined in ES module scopeWhy, and the fix
__dirname and __filename belong to CommonJS; ES modules do not have them. Use import.meta.dirname and import.meta.filename, which are stable since v24.0.0: join(import.meta.dirname, 'config.json').
import {readFile} from 'node:fs/promises';
async function loadConfig() {
try {
return readFile('config.json', 'utf8');
} catch {
return '{}';
}
}
console.log(await loadConfig());
What Node.js prints
Error: ENOENT: no such file or directory, open 'config.json'Why, and the fix
return hands the promise out of the try before it rejects, so the catch never sees the error, and the rejection crashes the program with exit code 1. Write return await readFile(...): the rejection is then thrown inside the try, and the catch returns '{}'.
JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.