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.allSettled so 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 โ†’ AbortController to cancel stale requests.

๐ŸŽฏ Likely Interview Questions

  1. Difference between Promise.all, allSettled, race and any?
  2. Why does await inside forEach not work as expected?
  3. How do you run 1,000 API calls with at most 5 at a time? (Implement it.)
  4. How do you cancel a fetch request? How do you cancel it on unmount in React?
  5. Implement retry with exponential backoff. Why add jitter?
  6. Does Promise.race cancel the losing promises? โ€” No.
  7. What is an unhandled promise rejection and how do you handle it in Node?
  8. What is the difference between return promise and return await promise inside try?

โ† Classes & Design Patterns | Index | Next: Browser APIs & Web Performance โ†’