Chapter 21 โ Advanced Async Patterns
๐ Definition
Advanced async patterns are the techniques for controlling asynchronous work: running tasks in parallel or with limited concurrency, cancelling them, retrying with backoff, timing them out, and handling errors without losing any. Interviewers use these to separate people who can use async/await from people who can reason about it.
๐ The Four Promise Combinators
| Combinator | Resolves when | Rejects when | Use case |
|---|---|---|---|
Promise.all |
All fulfil (array of values) | Any rejects (fail fast) | Load user + orders + settings together |
Promise.allSettled |
All settle (array of {status, value/reason}) |
Never | Bulk operations where partial failure is fine |
Promise.race |
First settles (fulfil or reject) | First settles with rejection | Timeouts |
Promise.any |
First fulfils | All reject (AggregateError) |
Fastest mirror / fallback servers |
๐ป Code Example โ Sequential vs Parallel
// โ Sequential: ~3 seconds if each takes 1 s
const user = await getUser(id);
const orders = await getOrders(id);
const reviews = await getReviews(id);
// โ
Parallel: ~1 second โ the calls are independent
const [user2, orders2, reviews2] = await Promise.all([getUser(id), getOrders(id), getReviews(id)]);
// โ forEach does NOT wait for async callbacks
ids.forEach(async (id) => await save(id));
console.log("done"); // logs before any save finishes
// โ
Sequential loop when order matters
for (const id of ids) await save(id);
// โ
Parallel when it does not
await Promise.all(ids.map((id) => save(id)));๐ป Code Example โ Concurrency Limit (Promise Pool)
Calling an API for 10,000 IDs with Promise.all opens 10,000 requests at once โ you will hit rate limits or exhaust sockets. Limit concurrency:
async function promisePool(tasks, limit) {
const results = new Array(tasks.length);
let next = 0;
async function worker() {
while (next < tasks.length) {
const i = next++; // safe: JS is single-threaded between awaits
results[i] = await tasks[i]();
}
}
await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
return results;
}
const tasks = userIds.map((id) => () => fetch(`/api/users/${id}`).then((r) => r.json()));
const users = await promisePool(tasks, 5); // at most 5 requests in flight๐ป Code Example โ Timeout With Promise.race
function withTimeout(promise, ms) {
let timer;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms);
});
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
await withTimeout(fetch("/slow"), 3000);Note: race does not cancel the slow promise โ it only stops waiting for it. To really cancel, use AbortController.
๐ป Code Example โ Cancellation With AbortController
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch("/api/report", { signal: controller.signal });
console.log(await res.json());
} catch (err) {
if (err.name === "AbortError") console.log("Request cancelled");
else throw err;
} finally {
clearTimeout(timer);
}
// Shorter, modern version
await fetch("/api/report", { signal: AbortSignal.timeout(5000) });In React, abort in the effect cleanup so a stale response never overwrites newer state:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then((r) => r.json())
.then(setResults)
.catch((e) => { if (e.name !== "AbortError") setError(e); });
return () => controller.abort();
}, [query]);๐ป Code Example โ Retry With Exponential Backoff and Jitter
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function retry(fn, { retries = 3, baseMs = 300, shouldRetry = () => true } = {}) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt >= retries || !shouldRetry(err)) throw err;
const delay = baseMs * 2 ** attempt + Math.random() * 100; // 300, 600, 1200 โฆ + jitter
await sleep(delay);
}
}
}
await retry(() => paymentApi.charge(order), {
shouldRetry: (e) => e.status >= 500 || e.code === "ECONNRESET", // never retry 4xx
});Jitter prevents thousands of clients retrying at exactly the same moment (the thundering herd). Only retry idempotent operations, or send an idempotency key.
๐ป Code Example โ Deduplicating In-Flight Requests
const inFlight = new Map();
function getProduct(id) {
if (!inFlight.has(id)) {
const p = fetch(`/api/products/${id}`)
.then((r) => r.json())
.finally(() => inFlight.delete(id));
inFlight.set(id, p);
}
return inFlight.get(id); // 10 components asking at once โ 1 request
}๐ป Code Example โ Promisify a Callback API
function promisify(fn) {
return (...args) =>
new Promise((resolve, reject) => {
fn(...args, (err, result) => (err ? reject(err) : resolve(result)));
});
}
import fs from "node:fs";
const readFile = promisify(fs.readFile); // or: import { readFile } from "node:fs/promises"๐ป Code Example โ Error Handling Pitfalls
// 1. A promise created without await/catch โ unhandled rejection
async function bad() {
sendEmail(user); // if this rejects, nobody catches it
}
// 2. try/catch only catches what you await
try {
return fetchData(); // โ returned, not awaited โ rejection escapes this catch
} catch {}
try {
return await fetchData(); // โ
} catch {}
// 3. Global safety nets (log and exit in Node; let the process manager restart)
process.on("unhandledRejection", (reason) => { logger.error(reason); process.exit(1); });๐ป Code Example โ Promise.withResolvers (ES2024)
const { promise, resolve, reject } = Promise.withResolvers();
socket.once("ready", resolve);
socket.once("error", reject);
await promise;๐ Real-World Use
- Dashboards loading independent widgets โ
Promise.allSettledso one failing widget does not blank the page. - Bulk email or data migration โ promise pool with a concurrency of 5โ20.
- Payment gateway calls โ timeout + retry with backoff + idempotency key.
- Search-as-you-type โ
AbortControllerto cancel stale requests.
๐ฏ Likely Interview Questions
- Difference between
Promise.all,allSettled,raceandany? - Why does
awaitinsideforEachnot work as expected? - How do you run 1,000 API calls with at most 5 at a time? (Implement it.)
- How do you cancel a fetch request? How do you cancel it on unmount in React?
- Implement retry with exponential backoff. Why add jitter?
- Does
Promise.racecancel the losing promises? โ No. - What is an unhandled promise rejection and how do you handle it in Node?
- What is the difference between
return promiseandreturn await promiseinsidetry?
โ Classes & Design Patterns | Index | Next: Browser APIs & Web Performance โ