Warm-up · Activity 1 of 7
Warm-up from lesson I1.2. What does this print?
Promise.resolve(5)
.then((n) => n * 2)
.then((n) => console.log(n));// I1.5 · ~33 min · Intermediate
After this lesson you turn a timer, a callback API or a one-time event into a function that returns a promise, so the rest of your code can simply await it.
You will be able to
Warm-up · Activity 1 of 7
Promise.resolve(5)
.then((n) => n * 2)
.then((n) => console.log(n));Predict · Activity 2 of 7
console.log("1");
const p = new Promise((resolve) => {
console.log("2");
resolve();
});
p.then(() => console.log("4"));
console.log("3");Practice · Activity 3 of 7
function alarm(person, delay) {
return new Promise((resolve, reject) => {
if (delay < 0) {
____(new Error("negative delay"));
return;
}
setTimeout(() => resolve(`Wake up, ${person}!`), delay);
});
}Practice · Activity 4 of 7
Practice · Activity 5 of 7
const p = new Promise((resolve, reject) => {
resolve("first");
reject(new Error("second"));
resolve("third");
});
p.then((v) => console.log("fulfilled:", v), (e) => console.log("rejected:", e.message));Brain teaser · Activity 6 of 7
const p = new Promise((resolve) => {
resolve(Promise.reject(new Error("inner")));
});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
Each block turns a different kind of asynchronous work into a promise. wait() wraps setTimeout. translate() wraps an error-first callback: reject on an error, resolve with the result. once() turns the next click into a promise; the listener is added before the click. withResolvers lets a timer call resolve from outside, and Promise.try turns a synchronous throw into a rejection.
main.js
// 1. A timer: wait(ms, value) fulfils with value after ms milliseconds.
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
// 2. A callback API with error-first callbacks (a fake dictionary).
function lookUp(word, callback) {
setTimeout(() => {
if (word === "cat") callback(null, "Katze");
else callback(new Error(`no entry for ${word}`));
}, 10);
}
function translate(word) {
return new Promise((resolve, reject) => {
lookUp(word, (error, result) => {
if (error) reject(error);
else resolve(result);
});
});
}
// 3. A one-time event.
const once = (target, type) =>
new Promise((resolve) => target.addEventListener(type, resolve, {once: true}));
console.log(await wait(10, "timer done"));
console.log("cat:", await translate("cat"));
try {
await translate("dog");
} catch (error) {
console.log("dog:", error.message);
}
const button = new EventTarget();
const clicked = once(button, "click"); // listen first
setTimeout(() => button.dispatchEvent(new Event("click")), 10);
console.log("event:", (await clicked).type);
// 4. withResolvers: resolve from outside an executor.
const {promise, resolve} = Promise.withResolvers();
setTimeout(() => resolve("resolved from outside"), 10);
console.log(await promise);
// 5. Promise.try: a function that may throw before it returns anything.
const parse = (text) => JSON.parse(text);
console.log("try:", await Promise.try(parse, "{bad").catch((error) => error.name));
Run it with
node main.jsOutput
timer done
cat: Katze
dog: no entry for dog
event: click
resolved from outside
try: SyntaxErrorTab 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
config.js has readConfig(name, callback), which calls back with (null, value), or with an Error for an unknown name. Write readConfigAsync(name) so that it returns a promise: fulfilled with the value, or rejected with readConfig’s error. The starter returns from the callback, which does not give the caller anything.
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.
A return inside the callback goes back to readConfig, not to your caller. What should readConfigAsync return instead?
Return new Promise((resolve, reject) => { … }) and call readConfig inside it.
In the callback: if (error) reject(error); else resolve(value);
One way to solve it. Yours can look different and still pass the checks.
import {readConfig} from "./config.js";
export function readConfigAsync(name) {
return new Promise((resolve, reject) => {
readConfig(name, (error, value) => {
if (error) reject(error);
else resolve(value);
});
});
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
import {readConfig} from "./config.js";
export function readConfigAsync(name) {
return readConfig(name, (error, value) => value);
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readConfigAsync} from './main.js';
test('readConfigAsync returns a promise', () => {
const result = readConfigAsync('theme');
assert.ok(result instanceof Promise, `readConfigAsync returned ${result}, not a promise`);
});
test('A known name fulfils with its value', async () => {
const got = await readConfigAsync('size');
assert.equal(got, 14, `readConfigAsync('size') gave ${got}`);
});
test('An unknown name rejects with the callback error', async () => {
await assert.rejects(readConfigAsync('font'), {message: 'no config named font'}, 'readConfigAsync should reject for an unknown name');
});
config.js
// A callback API: calls back with (error, value) after 5 ms.
const configs = {theme: "dark", size: 14};
export function readConfig(name, callback) {
setTimeout(() => {
if (name in configs) callback(null, configs[name]);
else callback(new Error(`no config named ${name}`));
}, 5);
}
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
waitFor(target, type, ms) should fulfil with the event when target fires type within ms milliseconds, and otherwise reject with an Error whose message is no <type> within <ms> ms. The starter’s timer hands the Error to resolve, so a missing event looks like a success. Fix it.
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 timeout cases fulfil. Which of the two functions makes a promise fail?
resolve(new Error(…)) fulfils with an Error object as the value. Only reject makes the promise reject.
setTimeout(() => reject(new Error(`no ${type} within ${ms} ms`)), ms);
One way to solve it. Yours can look different and still pass the checks.
export function waitFor(target, type, ms) {
return new Promise((resolve, reject) => {
target.addEventListener(type, resolve, {once: true});
setTimeout(() => reject(new Error(`no ${type} within ${ms} ms`)), ms);
});
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
export function waitFor(target, type, ms) {
return new Promise((resolve, reject) => {
target.addEventListener(type, resolve, {once: true});
setTimeout(() => resolve(new Error(`no ${type} within ${ms} ms`)), ms);
});
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {waitFor} from './main.js';
test('An event in time fulfils with the event', async () => {
const door = new EventTarget();
const opened = waitFor(door, 'open', 50);
setTimeout(() => door.dispatchEvent(new Event('open')), 5);
const event = await opened;
assert.equal(event.type, 'open', `waitFor fulfilled with ${event}`);
});
test('No event in time rejects with no open within 20 ms', async () => {
const door = new EventTarget();
await assert.rejects(waitFor(door, 'open', 20), {message: 'no open within 20 ms'}, 'waitFor should reject when the event does not come');
});
test('An event after the time limit does not change the result', async () => {
const door = new EventTarget();
const late = waitFor(door, 'open', 10);
setTimeout(() => door.dispatchEvent(new Event('open')), 30);
await assert.rejects(late, {message: 'no open within 10 ms'}, 'the late event should not turn the rejection into a success');
});
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.jsfunction wait(ms) {
return new Promise(() => {
setTimeout(() => {
return "done";
}, ms);
});
}
console.log(await wait(10));
What Node.js prints
Warning: Detected unsettled top-level awaitWhy, and the fix
The return goes back to the timer, which ignores it, and the executor never receives resolve at all. The promise stays pending forever, so the top-level await never finishes and Node.js stops with exit code 13. Take resolve as the executor’s parameter and call it: new Promise((resolve) => setTimeout(() => resolve("done"), ms)).
function checkedWait(ms) {
return new Promise((resolve) => {
setTimeout(() => {
if (ms > 50) throw new Error("too long");
resolve(ms);
}, 0);
});
}
checkedWait(100)
.then((ms) => console.log("waited", ms))
.catch((error) => console.log("caught:", error.message));
What Node.js prints
Error: too longWhy, and the fix
Only a throw directly in the executor rejects the promise. This throw happens later, inside the timer’s callback, after the executor has returned, so no promise sees it: it is an uncaught error and the catch never runs. Report failures in callbacks with reject: take reject as a parameter and call reject(new Error("too long")); return.
const person = new Promise((resolve) => resolve("Ada", 36));
const [name, age] = await person;
console.log(name, age.toFixed(1));
What Node.js prints
TypeError: age.toFixed is not a functionWhy, and the fix
A promise holds exactly one value; resolve ignores every argument after the first. So person fulfils with the string "Ada", and destructuring a string gives its letters: name is "A", age is "d". Put several values into one: resolve({name: "Ada", age: 36}) or resolve(["Ada", 36]).
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.