Warm-up · Activity 1 of 7
Warm-up: plain synchronous code, no async yet. What does this print?
function a() {
console.log('a start');
b();
console.log('a end');
}
function b() {
console.log('b');
}
a();// I1.1 · ~32 min · Intermediate
After this lesson you can predict the order in which synchronous code, promise callbacks, await continuations and timers run, catch async errors where they really happen, and pick Promise.all or Promise.allSettled on purpose.
Lesson 1 of 6 in I1 Asynchronous JavaScript
You will be able to
Warm-up · Activity 1 of 7
function a() {
console.log('a start');
b();
console.log('a end');
}
function b() {
console.log('b');
}
a();Predict · Activity 2 of 7
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');Practice · Activity 3 of 7
async function run() {
console.log('run: start');
await null;
console.log('run: after await');
}
setTimeout(() => console.log('timeout'), 0);
run();
queueMicrotask(() => console.log('microtask'));
console.log('script end');Practice · Activity 4 of 7
async function load() {
throw new Error('boom');
}
try {
load();
} catch (err) {
console.log('caught:', err.message);
}
console.log('done');Practice · Activity 5 of 7
// ES module, so top-level await is allowed
const fetchUser = () => Promise.resolve('Ada');
const fetchPosts = () => Promise.reject(new Error('posts API down'));
const results = await Promise.____([fetchUser(), fetchPosts()]);
for (const r of results) {
if (r.status === 'fulfilled') console.log('ok:', r.value);
else console.log('failed:', r.reason.message);
}Brain teaser · Activity 6 of 7
async function getData() {
throw new Error('offline');
}
async function load() {
try {
return getData();
} catch (err) {
return 'fallback';
}
}
load().then(
(value) => console.log('value:', value),
(err) => console.log('error:', err.message)
);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
The letters show the order in which the lines print. The synchronous lines run first, A to C, including the start of work() up to its await. Then the microtask queue is drained: the then() callback, the rest of work(), and everything the top-level awaits wait for, because a promise that settles without a timer settles in microtasks. The timer’s task runs last, although it was scheduled second.
main.js
console.log('A sync');
setTimeout(() => console.log('H timer (a task)'), 0);
Promise.resolve().then(() => console.log('D then (a microtask)'));
async function work() {
console.log('B work starts (still sync)');
await null;
console.log('E work after await (a microtask)');
}
work();
console.log('C sync, end of the first job');
// Errors: await inside try, and allSettled to keep every outcome.
const fail = (message) => Promise.reject(new Error(message));
async function load() {
try {
return await fail('offline');
} catch (err) {
return `fallback (${err.message})`;
}
}
console.log('F load:', await load());
const results = await Promise.allSettled([Promise.resolve('news'), fail('weather down')]);
console.log('G allSettled:', results.map((r) => (r.status === 'fulfilled' ? r.value : r.reason.message)));
Run it with
node main.jsOutput
A sync
B work starts (still sync)
C sync, end of the first job
D then (a microtask)
E work after await (a microtask)
F load: fallback (offline)
G allSettled: [ 'news', 'weather down' ]
H timer (a task)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.
Exercise 1 of 2
safeLoad(load) calls the async function load and should fulfil with its value, or with "fallback" if load rejects. The starter returns load() inside the try without awaiting it, so a rejection slips past the catch. Fix it with one word.
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: safeLoad rejects with offline. When does load()’s promise reject: inside the try, or after safeLoad has left it?
The catch only sees a rejection that is thrown while the try is still running. Which keyword turns a rejection into a throw?
return await load();
One way to solve it. Yours can look different and still pass the checks.
export async function safeLoad(load) {
try {
return await load();
} catch {
return "fallback";
}
}
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 async function safeLoad(load) {
try {
return load();
} catch {
return "fallback";
}
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {safeLoad} from './main.js';
test('A load that works gives its value', async () => {
const got = await safeLoad(async () => 'data');
assert.equal(got, 'data', `safeLoad gave ${got}`);
});
test('A load that rejects gives fallback', async () => {
const got = await safeLoad(async () => {
throw new Error('offline');
}).catch((error) => `rejected with ${error.message}`);
assert.equal(got, 'fallback', `safeLoad gave: ${got}`);
});
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
summarize(promises) should wait for every promise and fulfil with {ok, failed}: ok holds the values of those that fulfilled, failed the messages of those that rejected, both in input order. The starter uses Promise.all, so one failure rejects everything. Use the combinator that never rejects.
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: summarize rejects with weather down. Which combinator waits for every input and never rejects?
Promise.allSettled gives one object per input: {status: "fulfilled", value} or {status: "rejected", reason}.
Loop over the results: push result.value to ok, or result.reason.message to failed.
One way to solve it. Yours can look different and still pass the checks.
export async function summarize(promises) {
const results = await Promise.allSettled(promises);
const ok = [];
const failed = [];
for (const result of results) {
if (result.status === "fulfilled") ok.push(result.value);
else failed.push(result.reason.message);
}
return {ok, failed};
}
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 async function summarize(promises) {
const values = await Promise.all(promises);
return {ok: values, failed: []};
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {summarize} from './main.js';
const fail = (message) => Promise.reject(new Error(message));
test('Everything works: every value in ok', async () => {
const got = await summarize([Promise.resolve('news'), Promise.resolve('stocks')]);
assert.deepEqual(got, {ok: ['news', 'stocks'], failed: []}, `summarize gave ${JSON.stringify(got)}`);
});
test('Failures are reported, and the rest is kept', async () => {
const got = await summarize([Promise.resolve('news'), fail('weather down'), fail('stocks down')]).catch((error) => `rejected with ${error.message}`);
assert.deepEqual(got, {ok: ['news'], failed: ['weather down', 'stocks down']}, `summarize gave ${JSON.stringify(got)}`);
});
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.jslet data;
setTimeout(() => {
data = {items: [1, 2]};
}, 0);
console.log(data.items.length);
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'items')Why, and the fix
A timer callback is a task: it runs only after the current script has finished, even with a 0 ms delay. When the last line runs, data is still undefined. Use the data inside the callback, or wrap the timer in a promise and await it before reading data.
async function load() {
throw new Error("boom");
}
try {
load();
} catch (err) {
console.log("caught:", err.message);
}
console.log("done");
What Node.js prints
Error: boomWhy, and the fix
load() does not throw at the call: it returns a rejected promise, the try ends normally, “done” prints, and the rejection is never handled, so the program stops with it. Await it inside the try, in an async function or a module: try { await load(); } catch (err) { … }.
const results = await Promise.all([Promise.resolve(1), Promise.reject(new Error("x failed"))]);
console.log(results);
What Node.js prints
Error: x failedWhy, and the fix
Promise.all rejects with the first rejection, and an await of a rejected promise throws. Nothing catches it here, so the program stops before console.log. Wrap the await in try/catch, or use Promise.allSettled when every outcome matters.
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.