frontend / javascript core / promises-advanced.md

Promises — Advanced

4 min read source

Promises — Advanced

Builds on async_js/promises.md (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.

TL;DR

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.

Interview Q&A

Q: Which Promise.* combinator, when?

A:

Combinator Resolves when Rejects when Use for
all all fulfill any rejects (fail-fast) parallel fetches that all must succeed
allSettled all settle never “do all, report each outcome” (dashboards, batch)
race first to settle (fulfill or reject) first to reject if it’s first timeouts, “fastest source wins”
any first to fulfill only if all reject (AggregateError) redundant sources, ignore individual failures
// Timeout pattern with race
const data = await Promise.race([
  fetch(url),
  new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 5000)),
]);

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

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

// 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.

Q: Sequential vs concurrent — what’s the bug here?

A:

// 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.

Q: How do you catch errors that escape promises?

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

// browser
window.addEventListener("unhandledrejection", (e) => {
  report(e.reason);
  e.preventDefault();      // suppress default console error if handled
});
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.

Q: Can you cancel a promise?

A: 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:

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 ../11_apis_data_fetching/05_abort_and_race_conditions.md.

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

A: 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.

Q: What’s the microtask consequence of await?

A: Every await resumes via a microtask, so ordering interleaves with other microtasks — covered in event-loop.md. Two awaits back-to-back add two microtask hops even if both values are already resolved.

Gotchas / edge cases

  • 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 awaitarr.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.md).

What a senior is expected to say

  • all fail-fast, allSettled report-all, race first-settled, any first-fulfilled. I pick by the failure semantics.”
  • “Sequential await in a loop is a perf trap — Promise.all for independent work, a concurrency limiter when there are too many.”
  • “Promises aren’t cancellable; I cancel the work with AbortController and reject on the signal.”
  • “I always wire unhandledrejection into error reporting so swallowed rejections surface.”

Cross-references

Further reading