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 |
// 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:
// 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?
// 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:
// 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:
const c = new AbortController();
const data = fetch(url, { signal: c.signal });
c.abort(); // fetch rejects with AbortErrorFull 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.allfails fast but doesn’t cancel siblings — the others keep running; their results are discarded. Combine withAbortControllerif you must stop them.forEachdoesn’t await —arr.forEach(async …)fires all callbacks without waiting; usefor...ofwithawait, orPromise.all(map(...)).- Returning vs not returning in
.then— forgettingreturnbreaks the chain; the next.thengetsundefinedand doesn’t wait. asyncfunctions always return a promise — evenreturn 5becomesPromise.resolve(5);throwbecomes a rejection.finallydoesn’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 stringreasonwith no stack — always throwErrorobjects (Errors and Error Handling).
Related
Further reading
- MDN —
Promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise - MDN —
Promise.withResolvers: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers - MDN —
unhandledrejectionevent: https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event
Interview angle 4
- “Which
Promisecombinator, and why?” - pick by failure semantics.allrejects on the first failure,allSettledwaits for everything and reports each outcome,racesettles on the first result whether it resolved or rejected,anysettles on the first fulfilment and only rejects if they all fail.- Follow-up: “When does
racebite you?” - when you meantany. Araceagainst a fast failure gives you that rejection even though a slower call would have succeeded, which is rarely what you wanted for a fallback.
- Follow-up: “When does
- “What is wrong with
awaitinside a loop?” - it serialises independent work, so ten 100ms calls take a second instead of 100ms. Start them all andPromise.allthe results.- Follow-up: “And if there are ten thousand?” -
Promise.allfires 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.
- Follow-up: “And if there are ten thousand?” -
- “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
AbortControllersignal into the operation and have it reject with anAbortError. - “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.