Articles
JavaScript Promise Interview Questions: Ordering, Rejection, and Cancellation Boundaries
Practice JavaScript Promise interview questions on ordering, rejection, Promise combinators, AbortController boundaries, and deterministic tests.
JavaScript Promise interview questions are easiest when you stop treating “async” as one behavior. For every operation, separate when it starts, when it settles, where its value appears, who observes its rejection, and whether anything can actually stop the underlying work.
That distinction explains the traps behind Promise.all, Promise.any, Promise.allSettled, rejection chains, and AbortController. The central rule is simple: settling an aggregate Promise does not cancel its inputs. A strong interview answer states the contract, traces a concrete failure, and names the resource owner.
The five columns behind a strong Promise answer
Before choosing a combinator, draw five columns: input position, start, settlement, aggregate outcome, and cancellation owner.
Promise state is also more precise than everyday language. The ECMAScript specification distinguishes pending, fulfilled, and rejected states, while “resolved” means a Promise has been locked to another value or Promise and may still be pending. In most interviews, “settled” safely means fulfilled or rejected.
Promises do not schedule your operation for you. Calling fetchData() may start work immediately; passing its returned Promise to Promise.all only observes it. If concurrency must be limited, pass task functions to a scheduler and decide when to call each one.
Question 1: Does result order follow completion order?
No. Promise.all and Promise.allSettled produce arrays aligned with input iteration order, even when inputs finish in another order. “First” means something different for Promise.any and Promise.race: they react to the first qualifying settlement in time.
const result = await Promise.all([
delay(40, "A"),
delay(5, "B"),
delay(20, "C"),
]);
console.log(result); // ["A", "B", "C"]
The completion order is B, C, A, but the output positions remain A, B, C. State that before discussing the microtask queue. The adjacent JavaScript event loop guide covers scheduling in depth; here the interview signal is the aggregate's outcome contract.
Plain values are accepted too. The combinator treats them as fulfilled inputs, while a nonempty aggregate still settles asynchronously. Avoid saying the operations “run in parallel.” JavaScript can have concurrent pending work, but the actual execution model depends on the operation, host, and available resources.
Question 2: How do the four combinators differ under failure?
Use one scenario: inputs A, B, and C start in that order. B rejects at 10 ms, C fulfills at 20 ms, and A fulfills at 30 ms. The matrix below is an editorial trace derived from the official method contracts.
| Method | Aggregate outcome in this scenario | Output ordering | What happens to A and C? |
|---|---|---|---|
Promise.all | Rejects at 10 ms with B's reason | Fulfillment array would use input order | They keep running unless separately canceled |
Promise.allSettled | Fulfills after 30 ms with three outcome records | Records use input order | Every input is observed through settlement |
Promise.any | Fulfills at 20 ms with C's value | No result array; first fulfillment wins | A keeps running; B's rejection is accounted for |
Promise.race | Rejects at 10 ms with B's reason | No result array; first settlement wins | A and C keep running unless separately canceled |
Promise.all is fail-fast from the caller's perspective: the returned Promise rejects when an input rejects. That does not mean other inputs pause, roll back, or disappear. Their handlers remain attached, so later sibling rejections are consumed by the aggregate machinery, but any external effects they started can still happen.
Promise.any ignores individual rejections until one input fulfills. If every input rejects, it rejects with an AggregateError whose errors correspond to the inputs. Use it for interchangeable sources when one success is enough, then decide whether the losing operations should be aborted.
Promise.allSettled is appropriate when every outcome matters, such as showing which uploads succeeded. It does not convert failure into success semantically; it gives the caller structured records so the caller can make that decision.
Question 3: What happens with empty iterables?
Empty inputs expose whether a candidate knows the exact contract rather than a slogan.
| Expression | Result |
|---|---|
Promise.all([]) | Already fulfilled with [] |
Promise.allSettled([]) | Already fulfilled with [] |
Promise.any([]) | Already rejected with AggregateError |
Promise.race([]) | Remains pending forever |
Why does race([]) stay pending? No contestant can ever settle first. This can leak waiting work if an empty array reaches a timeout helper unexpectedly, so validate the input contract when “at least one contestant” is required.
An interviewer may ask whether an already-fulfilled aggregate invokes .then() synchronously. It does not. Promise reaction handlers run asynchronously; attaching a handler never calls it in the current synchronous stack.
Question 4: How do rejection, catch, and finally propagate?
A Promise chain is a sequence of newly returned Promises. A .then() handler that returns a value fulfills the next Promise with that value. Returning a Promise makes the next link adopt it. Throwing rejects the next link. If no rejection handler is present, the rejection travels down the chain until one handles it.
fetchUser()
.then(user => loadProfile(user.id))
.catch(error => ({ status: "fallback", error }))
.then(render);
Because the catch returns an object, the following link is fulfilled and render receives the fallback. If recovery is impossible, rethrow or return a rejected Promise. The interview question is not merely “where do you add catch?” It is “which layer owns recovery, logging, user state, and retry?”
finally is for cleanup that should run after either outcome. Its return value normally does not replace the chain's value. But if finally throws or returns a rejected Promise, the resulting chain rejects with that new reason. Do not hide business recovery inside cleanup.
Browsers can emit unhandledrejection when a rejection lacks a handler at the host's reporting point, and rejectionhandled if a handler appears later. Treat those events as diagnostics, not normal control flow. Attach intentional handlers at the ownership boundary.
Question 5: Why doesn't Promise.all cancel the other work?
A Promise represents a future outcome; it does not define a universal protocol for reversing the work that produced it. A database write, timer, computation, stream read, and HTTP request need different cleanup mechanisms. That is why the language's Promise abstraction has no first-class cancellation method.
For supported Web APIs such as fetch, AbortController provides an external cancellation signal. Pass the same signal to related requests, abort when the group no longer needs them, and still handle the resulting rejection.
async function firstHealthy(urls) {
if (urls.length === 0) throw new RangeError("at least one URL is required");
const controller = new AbortController();
const requests = urls.map(async url => {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`${response.status} from ${url}`);
return response.json();
});
try {
return await Promise.any(requests);
} finally {
controller.abort("winner selected or every request failed");
}
}
Two boundaries matter. First, fetch rejects for network failures or aborts, but an HTTP 404 or 500 still fulfills with a Response; checking response.ok turns an unacceptable status into your application-level rejection. Second, abort is a request to supported underlying work. A server may already have received the request or committed a side effect, so abort is not a transaction rollback.
Trace the abortable scenario without hand-waving
Suppose three replicas receive the request. Replica B returns valid JSON first, so Promise.any fulfills. The finally block aborts A and C. Their fetch Promises may reject with AbortError, but the aggregate has already attached rejection handlers to every input; you do not get three unrelated unhandled rejections from this design.
Now change the scenario: B returns HTTP 503. Because the code checks ok, B rejects at the application boundary and any continues waiting. If A later succeeds, A wins and C is aborted. If all three fail, any rejects with AggregateError, then finally still aborts anything that has not stopped.
This design needs another policy for non-idempotent operations. Racing three purchase requests could perform three purchases even if only one result wins. The safe answer is to avoid duplicate side effects, use idempotency keys where the service supports them, or select a different architecture. Promise combinators cannot manufacture transactional semantics.
Test ordering and cancellation deterministically
Timing tests that sleep for 10 or 20 milliseconds can pass for accidental reasons. Create deferred Promises whose resolve and reject functions the test controls. Start the aggregate, settle inputs in a deliberate order, and assert both the aggregate result and the later behavior of siblings.
function deferred() {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
For Promise.all, resolve input 3, then 1, then 2, and expect [value1, value2, value3]. In a second case, reject input 1, assert the aggregate rejects, then resolve input 2 and prove its cleanup or side effect still occurs. This directly tests the cancellation misconception.
For an abortable API wrapper, inject a fake operation that records the signal and listens for abort. Verify the winner is returned, losers receive the signal, HTTP-like failures follow the stated policy, and an empty list fails immediately. State the proof boundary: a mock verifies your orchestration contract, not browser networking or server rollback.
Common interview mistakes and stronger corrections
Calling Promise.all “parallel execution” is imprecise. Say that all supplied operations have already been started or observed and that the aggregate waits according to its contract. A scheduler is required to cap how many task functions start.
Saying “fail-fast cancels the rest” is wrong. Replace it with: “The aggregate rejects promptly; remaining work requires its own cancellation mechanism.” Saying “fetch rejects on 404” is also wrong. Define which HTTP statuses your application treats as failure and check the response.
Finally, do not use Promise.race([operation, timeout]) as proof that the operation stopped. A timeout Promise can win while the operation continues consuming memory, network, or a connection. Pair the timeout with an abort signal or explicit disposer when the operation supports one.
A compact answer template
When time is tight, answer in this order:
- Contract: name the combinator's fulfillment and rejection conditions.
- Ordering: distinguish input order from settlement order.
- Failure trace: walk through one rejected input and the aggregate outcome.
- Ownership: say who handles, retries, logs, or converts the rejection.
- Cancellation: name the underlying API's mechanism, if one exists.
- Proof: propose a deferred-Promise test with controlled settlement order.
This sequence is an interview technique, not an official scoring rubric. It works because every statement can be checked against code or a documented contract.
Prepare for JavaScript Promise interview questions
The best answer is not a memorized definition of async and await. It is a precise trace: inputs can settle out of order, ordered combinators preserve input positions, fail-fast settlement does not stop siblings, and cancellation belongs to the underlying operation and its owner.
Open the Frontend Engineer question collection, solve one Promise task with deferred inputs, and add a follow-up that aborts supported work without claiming rollback. If you can explain both the aggregate outcome and the work that remains alive, you are ready for the hard part of the interview.
Sources and Further Reading
- ECMAScript: Promise Objects
- MDN: Promise.all()
- MDN: Promise.allSettled()
- MDN: Promise.any()
- MDN: Promise.race()
- MDN: Using promises
- MDN: Using the Fetch API
- MDN: AbortController.abort()
Research note: This article was checked on September 14, 2026. JavaScript language behavior is sourced to current ECMAScript and MDN documentation; interview records are qualified separately.