JavaScript Promises: The Complete Interview Guide
A senior-level guide to JavaScript promises for interviews: the state machine, microtask timing, async/await, every combinator (all/allSettled/any/race), concurrency shapes, cancellation, retries, and the traps interviewers probe.
Short answer: A JavaScript promise is a one-way state machine — pending → fulfilled or rejected, settling at most once — that lets you attach callbacks (.then/.catch/.finally) which run as microtasks after the current synchronous code completes. async/await is syntax over the same machine. Interviews rarely test the happy path; they test timing (microtasks vs macrotasks), combinator semantics, and concurrency control.
This is the pillar for the async cluster. Each section links to a focused, worked deep-dive — use this page to build the mental model, then drill the individual problems.
The mental model: a settle-once state machine
A promise holds one of three states and moves in one direction only:
- pending — not settled yet.
- fulfilled — settled with a value.
- rejected — settled with a reason.
Once it leaves pending it is frozen: resolving an already-settled promise is a no-op. That single guarantee is why promises compose safely — a downstream .then can never see a value change out from under it.
const p = new Promise((resolve, reject) => {
resolve("first");
resolve("second"); // ignored — already settled
reject("error"); // ignored — already settled
});
p.then(console.log); // "first"The two-line version to say in an interview: "A promise is a placeholder for a future value that settles exactly once, and its reactions run on the microtask queue." If you can build one from scratch — states, then chaining, and async resolution — you understand the whole model. That's the build-a-Promise-from-scratch exercise, and it's the strongest single signal in a promises round.
Timing: why callbacks run "later"
Promise reactions are microtasks. The engine finishes the current synchronous stack, drains the entire microtask queue, and only then moves to the next macrotask (a setTimeout callback, an I/O event) or a render.
console.log("A");
setTimeout(() => console.log("B"), 0); // macrotask
Promise.resolve().then(() => console.log("C")); // microtask
console.log("D");
// A, D, C, BC beats B even though both are "zero delay," because the microtask queue is drained before the next macrotask. This is the event loop, and it is the source of most promise "what's the output?" questions. Master that page before combinators.
Consuming promises: then, catch, finally, await
.then(onFulfilled, onRejected) returns a new promise, which is what makes chains work: each link transforms the value and passes it on. .catch(fn) is sugar for .then(undefined, fn); .finally(fn) runs on either outcome and — critically — passes the value or rejection through untouched (a common polyfill trap).
async/await is the same machine with flatter syntax: await p suspends the async function until p settles, resuming on a microtask. Prefer it for sequential logic; reach for the combinators below when you need concurrency.
// Chaining vs async/await — identical semantics
fetchUser(id).then(u => fetchOrders(u.id)).then(render);
async function load(id) {
const u = await fetchUser(id);
render(await fetchOrders(u.id));
}The combinators
Four static methods aggregate multiple promises. Knowing exactly when each settles — and with what — is table stakes.
| Combinator | Settles when | Resolves with | Rejects when |
|---|---|---|---|
Promise.all | all fulfill or one rejects | array of values, in input order | first rejection (fail-fast) |
Promise.allSettled | all settle | array of {status, value/reason} | never |
Promise.any | first fulfillment or all reject | first fulfilled value | all reject → AggregateError |
Promise.race | first settle | first settled value | if the first to settle rejects |
The decision rule to say out loud:
- Need every result, abort on any failure?
Promise.all. - Need every outcome regardless of failures (dashboards, batch reports)?
Promise.allSettled. - Need the first success, tolerate failures (redundant mirrors)?
Promise.any. - Need the first to settle (timeouts, whichever-wins)?
Promise.race.
A classic follow-up: "Promise.all fails fast — but do the other promises stop?" No. Rejection settles the aggregate; the in-flight operations keep running. Stopping them is a cancellation problem (below), not a combinator feature.
Concurrency shapes: series, parallel, race, pool
Given a list of task functions (not already-running promises — you can't sequence what already started), interviewers ask you to control how many run at once:
- Series — one at a time, in order. Correct when tasks depend on each other or must not overload a resource.
- Parallel — start them all, wait for all. Fastest, but unbounded.
- Race — start all, take the first to settle.
All three are one worked page: run N async tasks (series · parallel · race). The senior escalation is bounded concurrency — "run at most k at a time" — the promise concurrency pool, and its scheduling cousin, batching by chunks. Unbounded Promise.all over 10,000 URLs is the bug a pool exists to fix; naming that trade-off is the senior signal.
Promise.withResolvers: the deferred pattern
Sometimes you need a promise you settle from outside the executor — bridging an event emitter, a one-shot signal, or a queue. Historically you hoisted resolve/reject out of the constructor (the "deferred" pattern):
function deferred() {
let resolve, reject;
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
return { promise, resolve, reject };
}ES2024 standardizes exactly this as Promise.withResolvers():
const { promise, resolve, reject } = Promise.withResolvers();
emitter.once("done", resolve);
emitter.once("error", reject);
await promise; // settles when the event firesUse it sparingly: a promise settled from the outside is harder to reason about than one whose lifetime is owned by its executor. The interview point is knowing the pattern exists, why it was error-prone to hand-roll (leaking resolve before assignment, double-settle), and that the language now provides it.
Error handling
- Every path needs a terminal
.catch(ortry/catcharoundawait). An unhandled rejection is a production incident, not a warning. .then(f).catch(g)catches errors fromftoo —catchsits downstream, so it sees rejections from every prior link..then(f, g)does not catch errors thrown insidef.finallynever swallows — it passes the settled value/rejection through. If yourfinallyreturns a value, it's ignored (unless it throws/rejects).- Rejections are values, not stack unwinds — a rejected promise nobody awaits just sits rejected; nothing "throws" until someone consumes it.
Building on the primitives
The promise round quickly becomes a composition round. These are the patterns interviewers layer on top:
- Auto-retry with backoff — wrap a task function, retry on rejection, cap attempts.
- Cancellation with AbortController — the standard way to stop in-flight work (the missing piece behind "does
Promise.allstop the others?"). - Promisify callback APIs — adapt
(err, data) => …Node callbacks into promises. - Memoize/cache async calls — dedupe in-flight requests and cache results (cache the promise, not just the value).
Common interview traps
forEachwithawaitdoesn't wait.arr.forEach(async x => await f(x))fires everything at once and ignores the awaits. Usefor…offor series, orPromise.all(arr.map(f))for parallel.- Rebuilding an array with spread in a loop (
acc = [...acc, await f(x)]) is O(n²). Push, or map. awaitin a loop when tasks are independent serializes needlessly — that's a parallelism bug, not a style nit.- Returning inside
.thenvs not — forgettingreturnin a chain link breaks value propagation and swallows errors. - Assuming
Promise.racecancels the losers. It doesn't; losers keep running (and can still reject → unhandled rejection).
Interviewer follow-ups to expect
- "Implement
Promise.all/allSettled/anyfrom scratch." (Each linked page walks the aggregation logic and edge cases like the empty array.) - "Your service calls 5,000 endpoints. Walk me from
Promise.allto a bounded pool — what breaks and why?" - "A user navigates away mid-request. How do you cancel, and what happens to the promise?"
- "Predict the output" — a mix of
setTimeout,Promise.resolve().then, andawait. (See the event loop.) - "What's the difference between
Promise.resolve(x)andnew Promise(r => r(x))?"
Senior / production implications
- Backpressure. Unbounded concurrency exhausts sockets, memory, and rate limits. Senior answers reach for a pool, a queue, or
allSettled+ partial success — not a biggerPromise.all. - Partial failure is a product decision. A dashboard tile failing shouldn't blank the page —
allSettled+ per-tile error states beats fail-fastall. - Cancellation is a lifecycle contract. Tie
AbortControllerto component unmount / route change so abandoned work stops consuming resources. - Unhandled rejections are observability signals. Wire
unhandledrejection(browser) /process.on('unhandledRejection')(Node) to your error tracker.
Where to go next
Build the primitive → learn the combinators → control concurrency → handle failure:
- Build a Promise from scratch — prove you understand the machine.
- Promise.all · allSettled · any · race · finally — the combinators.
- Run N async tasks → concurrency pool → batching — concurrency control.
- Retry · cancellation · promisify · async caching — production patterns.
Foundation: the event loop and closures.
Frequently asked questions
- What is a Promise in JavaScript?
- A Promise is an object representing the eventual result of an asynchronous operation. It is a state machine with three states — pending, fulfilled, or rejected — that transitions at most once. Callbacks registered with .then/.catch/.finally run on the microtask queue after the current synchronous code finishes.
- What is the difference between Promise.all, allSettled, any, and race?
- Promise.all resolves with all results or rejects on the first rejection (fail-fast). Promise.allSettled never rejects — it waits for every promise and reports each outcome. Promise.any resolves with the first fulfillment and rejects only if all reject (with an AggregateError). Promise.race settles as soon as the first promise settles, success or failure.
- Do promises run in parallel?
- Not in the CPU sense — JavaScript executes on one thread. Starting several promises 'in parallel' means their underlying async operations (network, timers, I/O) overlap while the main thread is free; their callbacks still run one at a time on the microtask queue.
- Why does a promise callback run after synchronous code?
- Promise reactions are queued as microtasks. The engine finishes the current synchronous call stack, then drains the entire microtask queue before rendering or the next macrotask (like setTimeout). That ordering is the source of most 'predict the output' interview questions.