Warm-up · Activity 1 of 7
Warm-up from B1.1: this program rejects a promise and nothing handles it. You run node main.js with no flags. What happens?
Promise.reject(new Error('lost'));
setTimeout(() => console.log('after'), 10);// B4.2 · ~32 min · Beginner
After this lesson you can await Node.js APIs with try/catch/finally, clean up in finally, tell when a rejection escapes your try and what Node.js does then, and rethrow errors with their cause.
You will be able to
Warm-up · Activity 1 of 7
Promise.reject(new Error('lost'));
setTimeout(() => console.log('after'), 10);Predict · Activity 2 of 7
import {readFile} from 'node:fs/promises';
async function load() {
try {
return await readFile('missing.txt', 'utf8');
} catch (error) {
console.log('catch', error.code);
throw error;
} finally {
console.log('finally');
}
}
try {
await load();
} catch {
console.log('outer');
}Practice · Activity 3 of 7
try {
await readFile('settings.json', 'utf8');
} catch (error) {
throw new Error('cannot read settings', {____: error});
}Practice · Activity 4 of 7
Practice · Activity 5 of 7
import {open} from 'node:fs/promises';
let file;
try {
file = await open('missing.txt');
console.log(await file.readFile('utf8'));
} catch (error) {
console.log(error.code);
} finally {
await file?.close();
console.log('closed');
}Brain teaser · Activity 6 of 7
import {readFile} from 'node:fs/promises';
import {setTimeout} from 'node:timers/promises';
const text = readFile('missing.json', 'utf8'); // starts the read now
await setTimeout(100); // something else first
try {
console.log(await text);
} catch (error) {
console.log('caught', error.code);
}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
loadJson opens a file, parses it and always closes it in finally, which prints what it did. Every failure is rethrown with a clear message and the original error as its cause. The loop at the top level awaits three files: a good one, a missing one and a broken one. Last, loadJson is called without await: its rejection reaches no catch, so the program listens for unhandledRejection, which would otherwise stop it with exit code 1.
main.js
import {open} from 'node:fs/promises';
// Reads a JSON file through a FileHandle, and always closes it.
async function loadJson(path) {
let file;
try {
file = await open(path, 'r');
return JSON.parse(await file.readFile('utf8'));
} catch (error) {
throw new Error('cannot load ' + path, {cause: error});
} finally {
await file?.close();
console.log(' finally:', file ? 'closed ' + path : 'nothing to close');
}
}
// Top-level await: main.js is an ES module, so no async function is needed.
for (const path of ['settings.json', 'missing.json', 'broken.json']) {
try {
console.log(path, '->', await loadJson(path));
} catch (error) {
console.log(path, '->', error.message, '| cause:', error.cause.code ?? error.cause.name);
}
}
// A promise nobody awaits: its rejection reaches no catch.
process.on('unhandledRejection', (reason) => {
console.log('unhandledRejection:', reason.message);
});
loadJson('missing.json');
settings.json
{"theme": "dark"}
broken.json
{theme: dark}
Run it with
node main.jsOutput
finally: closed settings.json
settings.json -> { theme: 'dark' }
finally: nothing to close
missing.json -> cannot load missing.json | cause: ENOENT
finally: closed broken.json
broken.json -> cannot load broken.json | cause: SyntaxError
finally: nothing to close
unhandledRejection: cannot load missing.jsonExercise 1 of 2
loadProfile(id, fetchProfile, log) awaits fetchProfile(id), which returns a promise. Make it return the profile; when fetchProfile rejects, throw new Error('cannot load profile ' + id) with the original error as its cause; and push 'done ' + id to log in every case, after a success and after a failure, using finally. This part runs in the browser too: fetchProfile is a stand-in for a slow service.
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.
Put return await fetchProfile(id) inside try. Without await, the rejection would skip your catch.
In catch: throw new Error('cannot load profile ' + id, {cause: error});
finally { log.push('done ' + id); } runs after the return and after the throw.
One way to solve it. Yours can look different and still pass the checks.
// fetchProfile(id) returns a promise of a profile. log collects what happened.
export async function loadProfile(id, fetchProfile, log) {
try {
return await fetchProfile(id);
} catch (error) {
throw new Error('cannot load profile ' + id, {cause: error});
} finally {
log.push('done ' + id);
}
}
// A stand-in for a slow service: id 1 exists, every other id fails.
const fakeFetch = (id) =>
new Promise((resolve, reject) => {
setTimeout(() => (id === 1 ? resolve({id, name: 'Ada'}) : reject(new Error('HTTP 404'))), 10);
});
const log = [];
console.log(await loadProfile(1, fakeFetch, log));
try {
await loadProfile(2, fakeFetch, log);
} catch (error) {
console.log(error.message, '<-', error.cause?.message);
}
console.log(log);
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
// fetchProfile(id) returns a promise of a profile. log collects what happened.
export async function loadProfile(id, fetchProfile, log) {
const profile = await fetchProfile(id);
return profile;
}
// A stand-in for a slow service: id 1 exists, every other id fails.
const fakeFetch = (id) =>
new Promise((resolve, reject) => {
setTimeout(() => (id === 1 ? resolve({id, name: 'Ada'}) : reject(new Error('HTTP 404'))), 10);
});
const log = [];
console.log(await loadProfile(1, fakeFetch, log));
try {
await loadProfile(2, fakeFetch, log);
} catch (error) {
console.log(error.message, '<-', error.cause?.message);
}
console.log(log);
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadProfile} from './main.js';
const found = async (id) => ({id, name: 'Ada'});
const notFound = async () => {
throw new Error('HTTP 404');
};
test('returns the profile', async () => {
const got = await loadProfile(1, found, []);
assert.deepEqual(got, {id: 1, name: 'Ada'}, `loadProfile returned ${JSON.stringify(got)}`);
});
test('a failure is thrown again with context', async () => {
await assert.rejects(loadProfile(2, notFound, []), {message: 'cannot load profile 2'}, 'expected the message "cannot load profile 2"');
});
test('the original error is the cause', async () => {
const original = new Error('HTTP 404');
let caught;
try {
await loadProfile(2, async () => {
throw original;
}, []);
} catch (error) {
caught = error;
}
assert.equal(caught?.cause, original, 'error.cause should be the very error that fetchProfile rejected with');
});
test('log gets done <id> after a success and after a failure', async () => {
const log = [];
await loadProfile(1, found, log);
await loadProfile(2, notFound, log).catch(() => {});
assert.deepEqual(log, ['done 1', 'done 2'], `log is ${JSON.stringify(log)}`);
});
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
firstLine(path, openFile) opens a file, reads it and returns its first line; openFile is open from node:fs/promises, and the tests pass a stand-in to count close() calls. Close the file in every case, also when reading fails. If opening fails with ENOENT, throw new Error(path + ': no such file') with the original error as its cause; rethrow every other error unchanged. Run node main.js and check with node --test.
This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.
Declare let file; before try, so that finally can see it.
finally { await file?.close(); } closes the file after return and after throw, and skips it when open failed.
In catch: if (error.code === 'ENOENT') throw new Error(path + ': no such file', {cause: error}); then throw error; for the rest.
One way to solve it. Yours can look different and still pass the checks.
import {open} from 'node:fs/promises';
// The first line of the file at path. openFile is open from node:fs/promises; the tests pass their own.
export async function firstLine(path, openFile = open) {
let file;
try {
file = await openFile(path, 'r');
const text = await file.readFile('utf8');
return text.split('\n')[0];
} catch (error) {
if (error.code === 'ENOENT') throw new Error(path + ': no such file', {cause: error});
throw error;
} finally {
await file?.close();
}
}
console.log(await firstLine('notes.txt'));
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 {open} from 'node:fs/promises';
// The first line of the file at path. openFile is open from node:fs/promises; the tests pass their own.
export async function firstLine(path, openFile = open) {
const file = await openFile(path, 'r');
const text = await file.readFile('utf8');
return text.split('\n')[0];
}
console.log(await firstLine('notes.txt'));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {firstLine} from './main.js';
test('reads the first line of notes.txt', async () => {
const got = await firstLine('notes.txt');
assert.equal(got, 'buy milk', `firstLine returned ${JSON.stringify(got)}`);
});
test('a missing file gets a clear message, with ENOENT as the cause', async () => {
let caught;
try {
await firstLine('missing.txt');
} catch (error) {
caught = error;
}
assert.equal(caught?.message, 'missing.txt: no such file', `the message was ${JSON.stringify(caught?.message)}`);
assert.equal(caught?.cause?.code, 'ENOENT', 'error.cause.code should be ENOENT');
});
test('the file is closed after reading', async () => {
let closed = 0;
const fakeOpen = async () => ({readFile: async () => 'a\nb\n', close: async () => {
closed++;
}});
const got = await firstLine('fake.txt', fakeOpen);
assert.equal(got, 'a', `firstLine returned ${JSON.stringify(got)}`);
assert.equal(closed, 1, `close() was called ${closed} time(s), expected once`);
});
test('the file is closed when reading fails, and the error passes through', async () => {
let closed = 0;
const fakeOpen = async () => ({
readFile: async () => {
throw new Error('read failed');
},
close: async () => {
closed++;
}
});
await assert.rejects(firstLine('fake.txt', fakeOpen), {message: 'read failed'}, 'the read error should pass through unchanged');
assert.equal(closed, 1, `close() was called ${closed} time(s), expected once`);
});
notes.txt
buy milk
water plants
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.jsimport {readFile} from 'node:fs/promises';
try {
readFile('missing.json', 'utf8').then((text) => console.log(text));
} catch (error) {
console.log('could not read', error.code);
}
console.log('after');
What Node.js prints
Error: ENOENT: no such file or directory, open 'missing.json'Why, and the fix
readFile returns a promise at once, and the try block ends before it rejects, so the catch never runs. The .then has no rejection handler either, so the rejection is unhandled: Node.js prints the error after "after" and exits with code 1. Await the promise inside the try: const text = await readFile(...).
import {readFile} from 'node:fs/promises';
['a.txt', 'b.txt'].forEach((name) => {
const text = await readFile(name, 'utf8');
console.log(text);
});
What Node.js prints
SyntaxError: Unexpected reserved wordWhy, and the fix
Top-level await works only in the body of the module itself. The arrow function given to forEach is a function of its own, and it is not async, so await is not allowed there and the whole file fails to load. Use a for...of loop at the top level: for (const name of names) { const text = await readFile(name, "utf8"); }.
import {open} from 'node:fs/promises';
let file;
try {
file = await open('missing.txt');
} finally {
await file.close();
}
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'close')Why, and the fix
open rejected with ENOENT, so file is still undefined when finally runs, and file.close() throws a TypeError. That new error replaces the ENOENT one, which is lost. Write await file?.close(): the ?. skips the call when there is no file, and the original error reaches you.
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.