AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

08 Asynchronous JavaScript / Lesson 4 of 5

Combinators, concurrency limits, timeouts and cancellation

14 min · +10 XP · 2 exercises · 5 questions

Read the text14 min
Exercises0/2
Quiz5 q

Quick answer

What is the difference between Promise.all, allSettled, race and any?

Promise.all fulfils with every value in input order and rejects on the first rejection. Promise.allSettled always fulfils, once everything settles, with a { status, value } or { status, reason } object for each input. Promise.race settles like the first input to settle, fulfilled or rejected. Promise.any fulfils with the first fulfilment and rejects with an AggregateError only if every input rejects.

Once every asynchronous operation is a promise, coordinating many of them becomes algebra: wait for all, wait for any, wait for the first, wait for all regardless of outcome. The four Promise combinators cover the common shapes; three patterns you write yourself cover the rest — a concurrency limit so a thousand requests do not all start at once, a timeout so nothing waits forever, and cancellation through AbortController so abandoned work stops. This lesson covers each, plus async iteration (for await) for sequences that arrive over time.

The four combinators

const [user, team] = await Promise.all([fetchUser(id), fetchTeam(tid)]);
CombinatorFulfils whenRejects whenResult
Promise.all(list)every promise fulfilsthe first rejection (others keep running, results dropped)array of values, in input order
Promise.allSettled(list)always, once all settleneverarray of { status: "fulfilled", value } / { status: "rejected", reason }
Promise.race(list)the first to settle fulfilsthe first to settle rejectsthat value / reason
Promise.any(list)the first to fulfilall reject → AggregateError with .errorsthat value

All four accept any iterable of promises or plain values (values are wrapped). all is the workhorse: fan out, wait, get results in order. allSettled is for batches where one failure must not hide the others. race is the building block for timeouts. any is "first success wins" — redundant sources, fastest mirror. An empty all/allSettled fulfils with [] immediately; an empty race stays pending forever; an empty any rejects.

Two details: rejection in all does not cancel the others — they finish and their results vanish; and each combinator attaches handlers to every input, so an input that rejects after all already rejected is still "handled" (no unhandled-rejection crash).

Bounded concurrency

Promise.all(urls.map(fetch)) starts every request at once — fine for five, a self-inflicted denial of service for five thousand. A pool runs at most n at a time:

async function mapLimit(items, limit, worker) {
  const results = new Array(items.length);
  let next = 0;
  async function runner() {
    while (next < items.length) {
      const i = next++;                       // claim an index — safe: no await between read and increment
      results[i] = await worker(items[i], i);
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runner));
  return results;
}

limit runners each pull the next index when they finish — a work queue with no explicit queue object. Results land by index, so order is preserved. Real code adds error policy (fail fast versus collect via the allSettled shape) and this is exactly what libraries like p-limit/p-map do.

Timeouts

function withTimeout(promise, ms, message = `timed out after ${ms} ms`) {
  let timer;
  const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), ms); });
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}

race against a timer. The finally clears the timer so a fast result does not leave a pending timeout keeping the process alive (and firing a rejection nobody handles — race handled it, but the timer itself still runs). Node 16 also has require("timers/promises").setTimeout(ms, value, { signal }) — a promise-based sleep that accepts an abort signal.

Cancellation: AbortController

Promises cannot be cancelled — a settled-once value has no "stop" — so cancellation is a signal passed into the operation, which checks it:

const controller = new AbortController();
setTimeout(() => controller.abort(), 500);                    // give up after half a second
try {
  const res = await fetch(url, { signal: controller.signal });    // fetch rejects with an AbortError on abort
} catch (err) {
  if (err.name === "AbortError") console.log("cancelled"); else throw err;
}

// honouring a signal in your own code
async function poll(fn, { signal }) {
  while (!signal.aborted) {
    await fn();
    await timersPromises.setTimeout(1000, undefined, { signal });   // sleeps that end early on abort
  }
  signal.throwIfAborted?.();                                        // Node 17.3+; on 16 throw manually
}

signal.aborted, signal.addEventListener("abort", …), and signal.reason (Node 17.2+) are the surface. fetch, timers/promises, fs.promises.readFile, streams and events.once all accept a signal. Abort rejects with an error named AbortError — treat it as "stop", not as failure. AbortSignal.timeout(ms) (Node 16.14+) builds the timeout case directly — on older runtimes combine a controller with a timer.

Retry with delay

async function retry(fn, { attempts = 3, delayMs = 100, isTransient = () => true } = {}) {
  for (let i = 1; ; i++) {
    try { return await fn(i); }
    catch (err) {
      if (i >= attempts || !isTransient(err)) throw err;
      await sleep(delayMs * 2 ** (i - 1));      // exponential backoff: 100, 200, 400 …
    }
  }
}

Module 7's rules — transient only, idempotent only, bounded — plus a delay that grows (and, in production, a little random jitter so a thousand clients do not retry in lockstep).

Async iteration

for await (const chunk of stream) { … }          // any object with [Symbol.asyncIterator]
async function* pages(url) {                     // an async generator: yield inside, await allowed
  while (url) { const page = await fetchJson(url); yield* page.items; url = page.next; }
}
for await (const item of pages(first)) { if (enough(item)) break; }   // break → return() → cleanup

for await awaits each next(); it also accepts a sync iterable of promises, awaiting each in order. Async generators are the natural producer for paginated APIs, line-by-line file reading (readline), and event streams (events.on(emitter, "data") returns one). Items are processed sequentially — one at a time, in order — which is often the point.

Events to promises

const [value] = await events.once(emitter, "ready") (Node) resolves on the first emission and rejects on "error". For a browser EventTarget, wrap addEventListener(…, { once: true }) in new Promise. Streams: stream/promises pipeline and finished.

Common mistakes

  • Promise.all for a batch where partial results matter (use allSettled); expecting all to cancel the rest on failure.
  • Unbounded map(fetch) over large lists.
  • A timeout race that does not clear its timer; a timeout that does not abort the underlying operation (it keeps running).
  • Treating AbortError as a failure to log loudly.
  • Retrying without backoff, jitter or a bound.
  • for await when concurrency was wanted (it is sequential by design).

Interview angle

  • "Promise.all versus allSettled?" all rejects on the first failure and drops the rest; allSettled always fulfils with every outcome.
  • "How do you limit concurrency?" A pool of n runners pulling from a shared index/queue; results stored by index.
  • "How do you add a timeout to a promise?" race against a timer, clear the timer in finally, and abort the operation with a signal if it supports one.
  • "How do you cancel a promise?" You cannot; pass an AbortSignal the operation honours and reject with AbortError.
  • "race versus any?" First to settle (including rejection) versus first to fulfil.

Key takeaways

  • all (values in order, fail fast), allSettled (every outcome), race (first settle), any (first fulfil, AggregateError if none).
  • Bound concurrency with a runner pool; it preserves order by index.
  • Timeout = race + timer + clearTimeout in finally; cancellation = AbortController → AbortError, honoured by fetch, timers and fs.
  • Retry transient, idempotent operations with bounded exponential backoff and jitter.
  • for await and async generators process sequences that arrive over time, one item at a time.

Common questions

Does Promise.all cancel the other promises when one rejects?

No. Promise.all rejects as soon as one input rejects, but the other operations keep running to completion and their results are dropped. Promises cannot be cancelled; stopping the work needs an AbortSignal that the operations honour.

How do you limit concurrency with promises in JavaScript?

Run a pool: start limit async runners that each claim the next index from a shared counter, await the worker for that item and store the result at that index, then await Promise.all over the runners. At most limit tasks run at once and results keep their order — what p-limit and p-map do.

How do you add a timeout to a promise?

Race it against a timer that rejects — Promise.race([promise, timeout]) — and clear the timer in finally so a fast result does not leave it pending. The race only stops the waiting; pass an AbortSignal as well if the operation itself should stop.

How do you cancel a promise in JavaScript?

You cannot cancel the promise itself. Pass an AbortController's signal into the operation — fetch, timers/promises, fs.promises.readFile and streams accept one — and call controller.abort(); the operation rejects with an error named AbortError, which means stop, not failure.

What does for await…of do in JavaScript?

for await (const x of source) loops over an async iterable, awaiting each next(), so items are processed one at a time and in order. It suits paginated APIs, streams and line-by-line file reading, usually fed by an async generator, and it also accepts a sync iterable of promises.

Mark as readSign in to keep your place.

Promise.all vs allSettled vs race vs any in JavaScript

Promise.all fails fast, allSettled reports every outcome, race takes the first to settle and any the first to fulfil. Plus concurrency limits and cancelling.

  • Course: JavaScript study plan
  • Module: Asynchronous JavaScript
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: Node 16

What is the difference between Promise.all, allSettled, race and any?

Promise.all fulfils with every value in input order and rejects on the first rejection. Promise.allSettled always fulfils, once everything settles, with a { status, value } or { status, reason } object for each input. Promise.race settles like the first input to settle, fulfilled or rejected. Promise.any fulfils with the first fulfilment and rejects with an AggregateError only if every input rejects.

Lesson

Once every asynchronous operation is a promise, coordinating many of them becomes algebra: wait for all, wait for any, wait for the first, wait for all regardless of outcome. The four Promise combinators cover the common shapes; three patterns you write yourself cover the rest — a concurrency limit so a thousand requests do not all start at once, a timeout so nothing waits forever, and cancellation through AbortController so abandoned work stops. This lesson covers each, plus async iteration (for await) for sequences that arrive over time.

The four combinators

const [user, team] = await Promise.all([fetchUser(id), fetchTeam(tid)]);
CombinatorFulfils whenRejects whenResult
Promise.all(list)every promise fulfilsthe first rejection (others keep running, results dropped)array of values, in input order
Promise.allSettled(list)always, once all settleneverarray of { status: "fulfilled", value } / { status: "rejected", reason }
Promise.race(list)the first to settle fulfilsthe first to settle rejectsthat value / reason
Promise.any(list)the first to fulfilall reject → AggregateError with .errorsthat value

All four accept any iterable of promises or plain values (values are wrapped). all is the workhorse: fan out, wait, get results in order. allSettled is for batches where one failure must not hide the others. race is the building block for timeouts. any is "first success wins" — redundant sources, fastest mirror. An empty all/allSettled fulfils with [] immediately; an empty race stays pending forever; an empty any rejects.

Two details: rejection in all does not cancel the others — they finish and their results vanish; and each combinator attaches handlers to every input, so an input that rejects after all already rejected is still "handled" (no unhandled-rejection crash).

Bounded concurrency

Promise.all(urls.map(fetch)) starts every request at once — fine for five, a self-inflicted denial of service for five thousand. A pool runs at most n at a time:

async function mapLimit(items, limit, worker) {
  const results = new Array(items.length);
  let next = 0;
  async function runner() {
    while (next < items.length) {
      const i = next++;                       // claim an index — safe: no await between read and increment
      results[i] = await worker(items[i], i);
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runner));
  return results;
}

limit runners each pull the next index when they finish — a work queue with no explicit queue object. Results land by index, so order is preserved. Real code adds error policy (fail fast versus collect via the allSettled shape) and this is exactly what libraries like p-limit/p-map do.

Timeouts

function withTimeout(promise, ms, message = `timed out after ${ms} ms`) {
  let timer;
  const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), ms); });
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}

race against a timer. The finally clears the timer so a fast result does not leave a pending timeout keeping the process alive (and firing a rejection nobody handles — race handled it, but the timer itself still runs). Node 16 also has require("timers/promises").setTimeout(ms, value, { signal }) — a promise-based sleep that accepts an abort signal.

Cancellation: AbortController

Promises cannot be cancelled — a settled-once value has no "stop" — so cancellation is a signal passed into the operation, which checks it:

const controller = new AbortController();
setTimeout(() => controller.abort(), 500);                    // give up after half a second
try {
  const res = await fetch(url, { signal: controller.signal });    // fetch rejects with an AbortError on abort
} catch (err) {
  if (err.name === "AbortError") console.log("cancelled"); else throw err;
}

// honouring a signal in your own code
async function poll(fn, { signal }) {
  while (!signal.aborted) {
    await fn();
    await timersPromises.setTimeout(1000, undefined, { signal });   // sleeps that end early on abort
  }
  signal.throwIfAborted?.();                                        // Node 17.3+; on 16 throw manually
}

signal.aborted, signal.addEventListener("abort", …), and signal.reason (Node 17.2+) are the surface. fetch, timers/promises, fs.promises.readFile, streams and events.once all accept a signal. Abort rejects with an error named AbortError — treat it as "stop", not as failure. AbortSignal.timeout(ms) (Node 16.14+) builds the timeout case directly — on older runtimes combine a controller with a timer.

Retry with delay

async function retry(fn, { attempts = 3, delayMs = 100, isTransient = () => true } = {}) {
  for (let i = 1; ; i++) {
    try { return await fn(i); }
    catch (err) {
      if (i >= attempts || !isTransient(err)) throw err;
      await sleep(delayMs * 2 ** (i - 1));      // exponential backoff: 100, 200, 400 …
    }
  }
}

Module 7's rules — transient only, idempotent only, bounded — plus a delay that grows (and, in production, a little random jitter so a thousand clients do not retry in lockstep).

Async iteration

for await (const chunk of stream) { … }          // any object with [Symbol.asyncIterator]
async function* pages(url) {                     // an async generator: yield inside, await allowed
  while (url) { const page = await fetchJson(url); yield* page.items; url = page.next; }
}
for await (const item of pages(first)) { if (enough(item)) break; }   // break → return() → cleanup

for await awaits each next(); it also accepts a sync iterable of promises, awaiting each in order. Async generators are the natural producer for paginated APIs, line-by-line file reading (readline), and event streams (events.on(emitter, "data") returns one). Items are processed sequentially — one at a time, in order — which is often the point.

Events to promises

const [value] = await events.once(emitter, "ready") (Node) resolves on the first emission and rejects on "error". For a browser EventTarget, wrap addEventListener(…, { once: true }) in new Promise. Streams: stream/promises pipeline and finished.

Common mistakes

  • Promise.all for a batch where partial results matter (use allSettled); expecting all to cancel the rest on failure.
  • Unbounded map(fetch) over large lists.
  • A timeout race that does not clear its timer; a timeout that does not abort the underlying operation (it keeps running).
  • Treating AbortError as a failure to log loudly.
  • Retrying without backoff, jitter or a bound.
  • for await when concurrency was wanted (it is sequential by design).

Interview angle

  • "Promise.all versus allSettled?" all rejects on the first failure and drops the rest; allSettled always fulfils with every outcome.
  • "How do you limit concurrency?" A pool of n runners pulling from a shared index/queue; results stored by index.
  • "How do you add a timeout to a promise?" race against a timer, clear the timer in finally, and abort the operation with a signal if it supports one.
  • "How do you cancel a promise?" You cannot; pass an AbortSignal the operation honours and reject with AbortError.
  • "race versus any?" First to settle (including rejection) versus first to fulfil.

Key takeaways

  • all (values in order, fail fast), allSettled (every outcome), race (first settle), any (first fulfil, AggregateError if none).
  • Bound concurrency with a runner pool; it preserves order by index.
  • Timeout = race + timer + clearTimeout in finally; cancellation = AbortController → AbortError, honoured by fetch, timers and fs.
  • Retry transient, idempotent operations with bounded exponential backoff and jitter.
  • for await and async generators process sequences that arrive over time, one item at a time.

Common questions

Does Promise.all cancel the other promises when one rejects?

No. Promise.all rejects as soon as one input rejects, but the other operations keep running to completion and their results are dropped. Promises cannot be cancelled; stopping the work needs an AbortSignal that the operations honour.

How do you limit concurrency with promises in JavaScript?

Run a pool: start limit async runners that each claim the next index from a shared counter, await the worker for that item and store the result at that index, then await Promise.all over the runners. At most limit tasks run at once and results keep their order — what p-limit and p-map do.

How do you add a timeout to a promise?

Race it against a timer that rejects — Promise.race([promise, timeout]) — and clear the timer in finally so a fast result does not leave it pending. The race only stops the waiting; pass an AbortSignal as well if the operation itself should stop.

How do you cancel a promise in JavaScript?

You cannot cancel the promise itself. Pass an AbortController's signal into the operation — fetch, timers/promises, fs.promises.readFile and streams accept one — and call controller.abort(); the operation rejects with an error named AbortError, which means stop, not failure.

What does for await…of do in JavaScript?

for await (const x of source) loops over an async iterable, awaiting each next(), so items are processed one at a time and in order. It suits paginated APIs, streams and line-by-line file reading, usually fed by an async generator, and it also accepts a sync iterable of promises.

Exercises

The four combinators on one set of tasks

Each line is name ms ok|fail; start(spec) (in the starter) returns a promise that fulfils with the name or rejects with Error("<name> failed") after ms. Run fresh promises through each combinator and print: all: <names joined by ,> or all: rejected <message>; allSettled: <name:status ...>; race: <name> fulfilled or race: <name> rejected; any: <name> or any: AggregateError with <n> errors.

Example: a 30 ok, b 10 ok, c 20 fail →

all: rejected c failed
allSettled: a:fulfilled b:fulfilled c:rejected
race: b fulfilled
any: b

A concurrency pool and a timeout

Write mapLimit(items, limit, worker) — at most limit workers run at once, results stored by index — and withTimeout(promise, ms) — a race against a timer that rejects with Error("timed out after <ms> ms"), clearing the timer in finally. Line 1 is the limit; following lines are tasks name ms, plus one timeout <taskMs> <limitMs> line. Run the tasks through the pool (each worker sleeps ms, tracks the running count, records completion order and returns the upper-cased name) and print results=<in input order> completed=<completion order> maxConcurrent=<n>; then run withTimeout(sleep(taskMs, "finished"), limitMs) and print timeout: finished or timeout: <message>.

Example: 2, a 40, b 10, c 15, d 5, timeout 50 10 →

results=A,B,C,D completed=b,c,d,a maxConcurrent=2
timeout: timed out after 10 ms

b finishes at 10 and frees a slot for c (done at 25), then d (30); a runs until 40.

In this module: Asynchronous JavaScript

← async/await — promises with the syntax of ordinary code · Timers and scheduling — setTimeout, setInterval, nextTick, and debounce/throttle →