01 · Advanced Async Patterns¶
🎥 Video walkthrough¶
Level 2 covered Promise and async/await for single asynchronous
operations. Real backend code usually has to juggle several async operations
at once — running them in parallel, racing them, or tolerating some of them
failing. This module covers the combinator methods on Promise, async
iteration, and generator-based async flows.
Promise.all — run in parallel, fail fast¶
Promise.all runs promises concurrently and resolves with an array of
results in the same order they were passed in. If any promise rejects,
Promise.all rejects immediately with that error — the other promises keep
running, but their results are discarded.
function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => resolve({ id, name: `User ${id}` }), 50);
});
}
function fetchOrders(userId) {
return new Promise((resolve) => {
setTimeout(() => resolve([`order-${userId}-1`, `order-${userId}-2`]), 30);
});
}
async function loadDashboard(userId) {
const [user, orders] = await Promise.all([
fetchUser(userId),
fetchOrders(userId),
]);
return { user, orders };
}
loadDashboard(7).then(console.log);
// { user: { id: 7, name: 'User 7' }, orders: [ 'order-7-1', 'order-7-2' ] }
// Both requests ran concurrently — total wait is ~50ms, not ~80ms.
async function failingAll() {
try {
await Promise.all([
Promise.resolve(1),
Promise.reject(new Error("second call failed")),
Promise.resolve(3),
]);
} catch (error) {
console.log("caught:", error.message); // caught: second call failed
}
}
failingAll();
Promise.allSettled — wait for everything, fail-safe¶
Promise.allSettled never short-circuits — it waits for every promise to
finish and reports the outcome of each one, whether it fulfilled or rejected.
Use it when you want partial results even if some operations fail (e.g.
pinging several services and reporting which ones are up).
async function checkServices() {
const results = await Promise.allSettled([
Promise.resolve("auth-service: ok"),
Promise.reject(new Error("payments-service: timeout")),
Promise.resolve("search-service: ok"),
]);
for (const result of results) {
if (result.status === "fulfilled") {
console.log("OK:", result.value);
} else {
console.log("FAILED:", result.reason.message);
}
}
}
checkServices();
// OK: auth-service: ok
// FAILED: payments-service: timeout
// OK: search-service: ok
Promise.race and Promise.any¶
Promise.race settles as soon as the first promise settles — fulfilled or
rejected. Promise.any settles as soon as the first promise fulfills,
ignoring rejections unless every promise rejects (in which case it rejects
with an AggregateError).
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
async function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("timed out")), ms)
);
return Promise.race([promise, timeout]); // whichever settles first wins
}
withTimeout(delay(200, "slow response"), 50)
.then(console.log)
.catch((error) => console.log("race error:", error.message));
// race error: timed out — the 50ms timeout beat the 200ms delay
async function fastestMirror() {
const winner = await Promise.any([
delay(100, "mirror-a"),
delay(20, "mirror-b"),
delay(60, "mirror-c"),
]);
console.log("first to respond:", winner); // first to respond: mirror-b
}
fastestMirror();
Combinator cheat sheet¶
| Method | Settles when | Rejects when |
|---|---|---|
Promise.all |
all fulfill | any one rejects (immediately) |
Promise.allSettled |
all settle (fulfilled or rejected) | never — always fulfills |
Promise.race |
the first one settles (either way) | first settled one is a rejection |
Promise.any |
the first one fulfills | all reject (AggregateError) |
Async iterators and for-await-of¶
An async iterable is an object whose values arrive over time, each
wrapped in a promise. for await...of consumes them one at a time, awaiting
each promise automatically.
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
const asyncNumbers = {
[Symbol.asyncIterator]() {
let current = 0;
return {
next() {
current += 1;
if (current > 3) return Promise.resolve({ done: true });
return delay(20, { done: false, value: current });
},
};
},
};
async function consume() {
for await (const value of asyncNumbers) {
console.log("received:", value);
}
console.log("done");
}
consume();
// received: 1
// received: 2
// received: 3
// done
Async generators¶
An async generator (async function*) is the ergonomic way to build an
async iterable — it can await inside the generator body and yield values
as they become ready, which is exactly the shape you need for paginated API
calls or reading a large file in chunks.
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
async function* fetchPages(totalPages) {
for (let page = 1; page <= totalPages; page += 1) {
const data = await delay(15, `page-${page}-data`); // simulate a network call
yield data;
}
}
async function loadAll() {
const collected = [];
for await (const page of fetchPages(3)) {
collected.push(page);
}
console.log(collected); // [ 'page-1-data', 'page-2-data', 'page-3-data' ]
}
loadAll();
Generator-based async flows (the idea behind async/await)¶
Before async/await existed, libraries drove plain generators to express
async control flow: a generator yields a promise, a runner function awaits
it and resumes the generator with the result. Understanding this "runner"
pattern demystifies what async/await compiles down to conceptually.
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
function* workflow() {
const user = yield delay(20, { id: 1, name: "Ada" });
const orders = yield delay(20, [`order-${user.id}`]);
return { user, orders };
}
// A minimal runner that drives any generator of this shape to completion.
function run(generatorFn) {
const iterator = generatorFn();
function step(input) {
const { value, done } = iterator.next(input);
if (done) return Promise.resolve(value);
return Promise.resolve(value).then(step); // await the yielded promise, then resume
}
return step();
}
run(workflow).then(console.log);
// { user: { id: 1, name: 'Ada' }, orders: [ 'order-1' ] }
How It Actually Works¶
Promise.all and Promise.race are built entirely on the microtask machinery already
covered: Promise.all creates one wrapper promise and attaches a .then reaction to
every input promise; each reaction, when it fires, records its result at the matching
index and decrements an internal counter, resolving the wrapper only when the counter
hits zero — a single rejection reaction calls the wrapper's reject function immediately,
but the other promises are not cancelled, they simply continue running to
completion with their results discarded (JavaScript promises have no built-in
cancellation — this is why AbortController exists as a separate, manually-checked
signal, not a promise feature).
Async generators (async function*) combine two desugaring transforms: the generator
transform (pause/resume via yield, implemented as a state machine V8 builds from your
function body, with local variables lifted into a heap-allocated context object so
they survive across suspend points) and the async/promise transform layered on top —
each for await...of iteration calls .next() on the generator, gets back a promise,
awaits it, and only then evaluates whether to continue. This means a for await loop
processes items strictly one at a time, in order, even if the underlying async iterable
could theoretically produce them out of order — the ordering guarantee comes entirely
from awaiting each .next() before requesting the next one.
Exercise¶
Write an async function pingAll(urls, timeoutMs) that takes an array of
"URLs" (simulate each with a delay() helper that resolves or randomly
rejects) and returns an array of { url, status: "ok" | "failed" | "timeout" }
objects — one per URL — using Promise.allSettled combined with a
per-request timeout built from Promise.race. No URL's failure should stop
the others from reporting.