Frontend / JavaScript core / 12_promises_advanced.md

Promises — Advanced

Updated 4 interview angles 5 min read source
On this page11
  1. Which Promise.* combinator, when?
  2. What is Promise.withResolvers() and why does it exist?
  3. Sequential vs concurrent — what’s the bug here?
  4. How do you catch errors that escape promises?
  5. Can you cancel a promise?
  6. Why does .then(f).catch(g) differ from .then(f, g)?
  7. What’s the microtask consequence of await?
  8. Gotchas
  9. Related
  10. Further reading
  11. Interview angle

Promises — Advanced

Builds on JavaScript Promises Interview Questions & Answers (states, chaining, .then/.catch/.finally). This file covers the parts seniors get grilled on: combinator selection, Promise.withResolvers, unhandled-rejection tracking, sequential-vs-concurrent control, and cancellation.

Pick the right combinator (all / allSettled / race / any) for the failure semantics you want. Use Promise.withResolvers() to get resolve/reject out of the executor. Track unhandledrejection so swallowed errors aren’t invisible. Know that promises aren’t cancellable themselves — you cancel the underlying work with AbortController. And never write a sequential await loop when the work could run concurrently.

Which Promise.* combinator, when?

Combinator Outcome Use for
all all fulfill; fails fast on any rejection parallel fetches that all must succeed
allSettled all settle; never rejects “do all, report each outcome”
race first to settle, fulfilled or rejected timeouts, “fastest source wins”
any first to fulfill; AggregateError if all reject redundant sources
js
// Timeout pattern with race
const data = await Promise.race([
  fetch(url),
  new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 5000)),
]);

What is Promise.withResolvers() and why does it exist?

It returns { promise, resolve, reject }, exposing the resolve/reject functions outside the executor — removing the old “capture them in outer variables” boilerplate:

js
// before
let resolve, reject;
const p = new Promise((res, rej) => { resolve = res; reject = rej; });

// ES2024
const { promise, resolve, reject } = Promise.withResolvers();

Handy for bridging event-based APIs (resolve when an event fires) and building deferreds/queues.

Sequential vs concurrent — what’s the bug here?

js
// SLOW — each await blocks the next; total ≈ sum of latencies
for (const id of ids) results.push(await fetchUser(id));

// FAST — fire all, await together; total ≈ max latency
const results = await Promise.all(ids.map(fetchUser));

The first is a common performance bug. Use Promise.all for independent work. When you need to bound concurrency (don’t fire 10k requests), use a pool / p-limit-style limiter, or a chunked loop.

How do you catch errors that escape promises?

A rejected promise with no .catch (or no try/catch around await) fires a global event:

js
// browser
window.addEventListener("unhandledrejection", (e) => {
  report(e.reason);
  // suppress default console error if handled
  e.preventDefault();
});
window.addEventListener("rejectionhandled", (e) => { /* a late .catch was attached */ });

// Node
process.on("unhandledRejection", (reason) => { /* log; in newer Node this can crash by default */ });

Wire these into your error reporter — silent rejections are how “it just stopped working” bugs hide.

Can you cancel a promise?

No — a promise has no cancel(). You cancel the operation behind it. The standard primitive is AbortController; pass its signal to fetch (and other signal-aware APIs), and reject on abort:

js
const c = new AbortController();
const data = fetch(url, { signal: c.signal });
c.abort();   // fetch rejects with AbortError

Full race-condition treatment (typeahead, generation counters, AbortSignal.timeout/any) lives in Race conditions and AbortController.

Why does .then(f).catch(g) differ from .then(f, g)?

In .then(f, g), g handles a rejection of the previous promise but not an error thrown inside f. .catch(g) (i.e., .then(undefined, g)) is chained after f, so it catches errors from both the source and f. Prefer the trailing .catch.

What’s the microtask consequence of await?

Every await resumes via a microtask, so ordering interleaves with other microtasks — covered in The Event Loop — Macrotasks, Microtasks, Rendering. Two awaits back-to-back add two microtask hops even if both values are already resolved.

Gotchas

  • Promise.all fails fast but doesn’t cancel siblings — the others keep running; their results are discarded. Combine with AbortController if you must stop them.
  • forEach doesn’t await — arr.forEach(async …) fires all callbacks without waiting; use for...of with await, or Promise.all(map(...)).
  • Returning vs not returning in .then — forgetting return breaks the chain; the next .then gets undefined and doesn’t wait.
  • async functions always return a promise — even return 5 becomes Promise.resolve(5); throw becomes a rejection.
  • finally doesn’t receive the value/reason and, if it returns a promise, delays settlement; if it throws, it overrides the outcome.
  • A thrown non-Error (throw "oops") gives you a string reason with no stack — always throw Error objects (Errors and Error Handling).

Further reading

Interview angle 4

  • “Which Promise combinator, and why?” - pick by failure semantics. all rejects on the first failure, allSettled waits for everything and reports each outcome, race settles on the first result whether it resolved or rejected, any settles on the first fulfilment and only rejects if they all fail.
    • Follow-up: “When does race bite you?” - when you meant any. A race against a fast failure gives you that rejection even though a slower call would have succeeded, which is rarely what you wanted for a fallback.
  • “What is wrong with await inside a loop?” - it serialises independent work, so ten 100ms calls take a second instead of 100ms. Start them all and Promise.all the results.
    • Follow-up: “And if there are ten thousand?” - Promise.all fires all of them at once and you hit connection limits or rate limits. That needs a concurrency limiter - a fixed pool of workers pulling from a queue.
  • “Can you cancel a promise?” - no. A promise is a value that will settle, and there is no cancel on it. You cancel the work - pass an AbortController signal into the operation and have it reject with an AbortError.
  • “Why wire up unhandledrejection?” - because a rejection with no handler is silent otherwise. It is the promise equivalent of an uncaught exception, and without the listener it never reaches your error reporter at all.