Warm-up · Activity 1 of 7
Warm-up from lesson I1.1: a callback given to setTimeout. What does this print?
console.log("order");
setTimeout(() => console.log("ready"), 0);
console.log("wait");// I1.2 · ~32 min · Intermediate
After this lesson you turn nested callbacks into a flat promise chain that waits for every step, handles every error in one catch() and cleans up in finally().
You will be able to
Warm-up · Activity 1 of 7
console.log("order");
setTimeout(() => console.log("ready"), 0);
console.log("wait");Predict · Activity 2 of 7
const first = Promise.resolve(2);
const second = first.then((n) => n * 10);
console.log(first === second);
second.then((n) => console.log(n));Practice · Activity 3 of 7
Promise.reject(new Error("timeout"))
.then((data) => console.log("data:", data))
.____((error) => console.log("failed:", error.message));Practice · Activity 4 of 7
Promise.resolve()
.then(() => console.log("step 1"))
.then(() => {
throw new Error("step 2 broke");
})
.then(() => console.log("step 3"))
.catch((error) => console.log("caught:", error.message))
.then(() => console.log("after the catch"))
.finally(() => console.log("cleanup"));Practice · Activity 5 of 7
const later = (value) => new Promise((resolve) => setTimeout(() => resolve(value), 10));
Promise.resolve("id-7")
.then((id) => later(`user for ${id}`))
.then((result) => console.log(typeof result, result));Brain teaser · Activity 6 of 7
const save = (x) => new Promise((resolve) => setTimeout(() => resolve(`saved ${x}`), 10));
Promise.resolve("draft")
.then((doc) => {
save(doc);
})
.then((result) => console.log("result:", result));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
findUser and findOrders each return a promise that settles a little later. The program shows that the call returns a promise at once, then chains the steps: each then() gets the value of the step before, the first returns the next promise so the chain waits for it, and catch() and finally() close the chain. The second half asks for user 2, which does not exist, so the rejection skips a then() and reaches the last catch().
main.js
// Two steps that finish later. Each returns a promise (lesson I1.5 shows how to make one).
function findUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => (id === 1 ? resolve({id, name: "Ada"}) : reject(new Error(`no user ${id}`))), 20);
});
}
function findOrders(user) {
return new Promise((resolve) => setTimeout(() => resolve([`${user.name}-A`, `${user.name}-B`]), 10));
}
const pending = findUser(1);
console.log("right after the call:", pending instanceof Promise);
pending
.then((user) => {
console.log("user:", user.name);
return findOrders(user); // return the next promise, so the chain waits for it
})
.then((orders) => {
console.log("orders:", orders);
return orders.length;
})
.then((count) => console.log("count:", count))
.catch((error) => console.log("failed:", error.message))
.finally(() => console.log("done with user 1"))
.then(() => findUser(2))
.then((user) => console.log("never printed:", user.name))
.catch((error) => console.log("failed:", error.message));
Run it with
node main.jsOutput
right after the call: true
user: Ada
orders: [ 'Ada-A', 'Ada-B' ]
count: 2
done with user 1
failed: no user 2Tab 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.
Exercise 1 of 2
orderTotal(fetchPrice, fetchShipping) should return a promise of price plus shipping. fetchPrice() returns a promise of the price; fetchShipping(price) returns a promise of the shipping cost for that price. The starter calls both, but its promise is fulfilled with undefined. Make the chain wait for the shipping and return the sum. A rejection of fetchPrice() must still reject orderTotal.
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.
Run the tests: the total is undefined. What does the outer callback return?
The callback has braces, so it returns only what a return statement gives it.
Write return in front of fetchShipping(price).then(…), so the outer then() waits for that promise.
One way to solve it. Yours can look different and still pass the checks.
// Resolve to price + shipping.
export function orderTotal(fetchPrice, fetchShipping) {
return fetchPrice().then((price) => {
return fetchShipping(price).then((shipping) => price + shipping);
});
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// Resolve to price + shipping.
export function orderTotal(fetchPrice, fetchShipping) {
return fetchPrice().then((price) => {
fetchShipping(price).then((shipping) => price + shipping);
});
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {orderTotal} from './main.js';
const later = (value) => new Promise((resolve) => setTimeout(() => resolve(value), 5));
const shipping = (price) => later(price >= 50 ? 0 : 4.5);
test('A price of 20 with 4.50 shipping gives 24.5', async () => {
const got = await orderTotal(() => later(20), shipping);
assert.equal(got, 24.5, `orderTotal gave ${got}, expected 24.5`);
});
test('A price of 60 ships free, so the total is 60', async () => {
const got = await orderTotal(() => later(60), shipping);
assert.equal(got, 60, `orderTotal gave ${got}, expected 60`);
});
test('A rejected price rejects the total with the same error', async () => {
const failing = () => Promise.reject(new Error('price service down'));
await assert.rejects(orderTotal(failing, shipping), {message: 'price service down'}, 'orderTotal should reject with the price error');
});
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
loadName(fetchUser, log) should return a promise of the user’s name in capitals, or GUEST when fetchUser() rejects or the user has no name. Either way it must call log("loaded") exactly once. The starter passes an error handler as then()’s second argument, which does not see errors thrown by the first, and it logs only on success. Use catch() and finally().
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.
Run the tests: the user without a name fails with a TypeError. Which handler should see it?
then(onOk, onFail): onFail handles only the promise then() was called on, not errors thrown in onOk.
Chain .catch(() => "GUEST") after the then(), and put log("loaded") in .finally(…), which runs on both paths.
One way to solve it. Yours can look different and still pass the checks.
// Resolve to the name in capitals, or GUEST; call log("loaded") once either way.
export function loadName(fetchUser, log) {
return fetchUser()
.then((user) => user.name.toUpperCase())
.catch(() => "GUEST")
.finally(() => log("loaded"));
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// Resolve to the name in capitals, or GUEST; call log("loaded") once either way.
export function loadName(fetchUser, log) {
return fetchUser()
.then((user) => user.name.toUpperCase(), () => "GUEST")
.then((name) => {
log("loaded");
return name;
});
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadName} from './main.js';
const run = async (fetchUser) => {
const lines = [];
const name = await loadName(fetchUser, (line) => lines.push(line));
return {name, lines};
};
test('A user named ada gives ADA and logs loaded once', async () => {
const {name, lines} = await run(() => Promise.resolve({name: 'ada'}));
assert.equal(name, 'ADA', `the name was ${name}, expected ADA`);
assert.deepEqual(lines, ['loaded'], `log got ${JSON.stringify(lines)}, expected ["loaded"]`);
});
test('A rejected fetchUser gives GUEST and logs loaded once', async () => {
const {name, lines} = await run(() => Promise.reject(new Error('offline')));
assert.equal(name, 'GUEST', `the name was ${name}, expected GUEST`);
assert.deepEqual(lines, ['loaded'], `log got ${JSON.stringify(lines)}, expected ["loaded"]`);
});
test('A user without a name gives GUEST and logs loaded once', async () => {
const {name, lines} = await run(() => Promise.resolve({}));
assert.equal(name, 'GUEST', `the name was ${name}, expected GUEST`);
assert.deepEqual(lines, ['loaded'], `log got ${JSON.stringify(lines)}, expected ["loaded"]`);
});
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 findUser = (id) => Promise.resolve({id, name: "Ada"});
const user = findUser(1);
console.log(user.name.toUpperCase());
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'toUpperCase')Why, and the fix
findUser returns a promise, not the user. A promise has no name property, so user.name is undefined. The value only exists inside a then() callback (or after await, lesson I1.3): findUser(1).then((user) => console.log(user.name.toUpperCase())).
Promise.resolve([3, 4])
.then((items) => {
items.map((n) => n * 2);
})
.then((doubled) => console.log(doubled.length));
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'length')Why, and the fix
A callback with curly braces returns undefined unless it says return, so the next step gets undefined. Nothing catches the TypeError, so the run stops with it. Write return items.map((n) => n * 2); or drop the braces. The same rule makes a chain wait: return the next promise.
const load = () => Promise.reject(new Error("offline"));
load().then((data) => console.log("data:", data));
console.log("started");
What Node.js prints
Error: offlineWhy, and the fix
Without a catch(), a rejection has nowhere to go. Node.js reports an unhandled rejection and stops with exit code 1, after “started” has printed; a browser logs it in the console. End every chain with catch(), or return the chain to a caller that handles it: load().then(…).catch((error) => console.log("failed:", error.message)).
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.