Promise.prototype.finally
intermediateImplement finally correctly: pass values through, and don't swallow rejections.
Implement Promise.race in JavaScript, use it for the timeout pattern with cleanup, and know why an empty race pends forever instead of resolving.
Implement
promiseRace(iterable): settle exactly like the first input promise that settles — fulfillment or rejection, whichever happens first.
The implementation is the shortest of the four combinators — settle-once does all the work. Which is precisely why interviewers ask it: the code takes two minutes, and the remaining twenty are about semantics (first-settle vs first-success), the empty-input quirk, and the timeout pattern built on top.
function promiseRace(iterable) {
return new Promise((resolve, reject) => {
for (const item of iterable) {
// Every contender gets both callbacks; the first to call
// either one wins, because a promise can only settle once.
Promise.resolve(item).then(resolve, reject);
}
});
}That's the entire mechanism: N subscriptions racing to call resolve/reject on one promise, and settle-once discards all but the first. If you've internalized how promises settle, there is genuinely nothing else.
Promise.race([]); // pends FOREVER — by specWith no contenders, nothing can ever settle. Many hand-rolled versions "gracefully" resolve undefined for empty input — that's not defensive coding, it's a spec violation that turns a detectable hang into silent wrong data. The empty-input row is where all four combinators differ, and it's a favorite rapid-fire question:
all([]) | allSettled([]) | any([]) | race([]) | |
|---|---|---|---|---|
| Result | resolves [] | resolves [] | rejects (AggregateError) | pends forever |
function withTimeout(promise, ms) {
let timerId;
const timeout = new Promise((_, reject) => {
timerId = setTimeout(
() => reject(new Error(`Timed out after ${ms}ms`)),
ms
);
});
return Promise.race([promise, timeout]).finally(() => {
clearTimeout(timerId); // don't leave a live timer after winning
});
}
const user = await withTimeout(fetchUser(), 3000);Two production details separate a senior answer:
finally is not decoration.AbortSignal.timeout(ms) is now the built-in that fuses both halves of this pattern.race([slowSuccess, fastFailure]) rejects. If you want failures to lose, that's Promise.any — first-settle vs first-success is the distinction this question exists to check.race([Promise.resolve(1), anything]) fulfills with 1 on the next microtask; "immediately" still means asynchronously (see the event loop).race([42, slowPromise]) → 42; Promise.resolve wrapping again.race abandons results, it doesn't cancel work. Repeat it until it sticks; it's the most common wrong belief about this API.for...of rather than .forEach on an assumed array.undefined (the "helpful" spec violation above).race as "first success" and being surprised by winning rejections.clearTimeout — a real bug in Node services (lingering timers delay process exit and pile up under load).race with flags (let settled = false; if (!settled) {...}) — redundant state that settle-once already provides; interviewers read it as not trusting the primitive.Promise.race([
new Promise((res) => setTimeout(res, 200, "slow win")),
new Promise((_, rej) => setTimeout(rej, 100, "fast fail")),
]).then((v) => console.log("then:", v))
.catch((e) => console.log("catch:", e));withTimeout without leaking the timer." — the pattern above; the question is specifically fishing for clearTimeout.AbortSignal, abort the rest when a winner settles — see AbortController.AbortSignal.timeout?" — the modern built-in for deadline + cancellation in one: fetch(url, { signal: AbortSignal.timeout(3000) }); knowing it exists dates your knowledge post-2022, which is the point of the question.race from scratch-promise primitives." — it's the four-line function above sitting on settle-once semantics; explaining why no extra state is needed is the actual answer.Implement finally correctly: pass values through, and don't swallow rejections.
Implement Promise.any with AggregateError semantics: first success wins, all failures reject.
Run N async tasks with at most K in flight — the production-style p-limit pattern, including ordering and failure trade-offs.
Implement allSettled: wait for every promise and report per-promise status objects.