Callbacks, Promises and async/await
async/await.
The third reads like synchronous code but runs on the same event loop as lesson 19. This
lesson teaches the current model and the failure modes that survive all three eras.
Promises: a handle for one future value
Three states, settled exactly once
A promise starts pending and settles once — fulfilled (has a value) or
rejected (has an error). Settlement is final: a second resolve is a
no-op. That guarantee is what makes chaining composable.
Fulfillment flows through .then; rejection skips ahead to the nearest .catch.
Chaining transforms values; rejections skip ahead
fetchLike(2)
.then((n) => n * 10) // return a value → next .then receives it
.then((n) => fetchLike(n)) // return a PROMISE → the chain waits for it
.then((n) => console.log("chain result:", n))
.catch((error) => console.log("caught:", error.message)); // covers EVERY step above
// Expected output:
// chain result: 31 (fetchLike(2)=3 → 3*10=30 → fetchLike(30)=31)
The four combinators, one job each
// p1, p2, p3 are promises.
Promise.all([p1, p2, p3]); // all must fulfill — one rejection rejects everything
Promise.allSettled([p1, p2]); // never rejects — reports every outcome
Promise.any([p1, p2]); // first FULFILLED wins (rejections ignored)
Promise.race([p1, p2]); // first SETTLED wins — even a rejection
async/await — sequential syntax, concurrent scheduling
The await keyword pauses the FUNCTION, not the thread
// SEQUENTIAL: 30ms + 40ms — the second waits for the first on purpose
const a = await fetchSlow(30);
const b = await fetchSlow(40); // starts only after a finished
// CONCURRENT: both requests are in flight immediately — total ≈ max, not sum
const [x, y] = await Promise.all([fetchSlow(30), fetchSlow(40)]);
// Expected timing (roughly):
// sequential ≈ 70ms · concurrent ≈ 40ms
Choosing between them is an architectural decision: depend on each other → sequential;
independent → concurrent. The single most common performance bug in async code is
await-ing in a loop what could be Promise.all-ed.
Errors: try/catch works — if you do not float the promise
async function loadProfile(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`); // lesson 17's trap
return await response.json();
} catch (error) {
console.error("loadProfile failed:", error.message); // covers BOTH awaits
return null;
}
}
Contraexample — the floating promise: calling
saveSettings(); without await (or .catch) detaches
the entire chain: no try/catch can see its failures, and since Node 15 an
unhandled rejection kills the process. Demo async/28_promises_lab.js shows the
crash and the fix.
Practice: from pyramid to async/await
The task
Run async/27_event_loop_lab.js and async/28_promises_lab.js with
node. Predict every log line before each runs. Then take this callback
pyramid and flatten it into async/await with a
try/catch — without changing what happens, only how it reads:
getUser(id, (err, user) => {
if (err) return console.error(err);
getPosts(user, (err, posts) => {
if (err) return console.error(err);
getComments(posts[0], (err, comments) => {
if (err) return console.error(err); // three levels deep and counting
console.log(comments.length);
});
});
});
The checklist
- You can predict any mixed sync/timer/promise log order — and explain it with the two queues.
- You know settlement is final, and one
catchcovers a whole chain. - You pick the combinator by its rejection behavior, not by habit.
- You never leave a promise floating: every call is awaited or has a
catch.