Writing · MARGIN NOTESFront-End Development
What Interviewers Mean When They Ask About Promises
Promises are not "async magic." They are a state machine plus a queue for what runs next. Here is the interview mental model for then, catch, async/await, and the gotchas that separate a pass from a stumble.
- Author
- Nicola Riker Urdaneta
- Published
- September 22, 2026
- Reading time
- 5 min read

Contents
In interviews, "explain promises" is rarely a request for the Promise constructor API list. Interviewers want to know whether you can predict when code runs after a then, how errors travel through a chain, and what async/await actually changes.
A promise is a placeholder for a future value. It has a small state machine. Your callbacks wait for a settlement, then run as microtasks. Once you own that model, async/await stops feeling like a different language.
Why interviewers ask
The prompt usually looks like one of these:
- Given a short then/catch chain, what prints, and in what order?
- Rewrite this callback pyramid with promises or
async/await. - What is the difference between awaiting in a loop and
Promise.all? - Why did this rejection become an
unhandled promise rejection?
They are testing four things at once:
- Do you know pending, fulfilled, and rejected, and that settlement is one-way?
- Do you know then/catch return new promises, so chaining is composition, not mutation of one promise?
- Can you explain
async/awaitas sugar over that same model, not a thread blocker? - Can you name a real production gotcha (forgotten return, swallowed error, serial awaits that should be parallel)?
A glossary answer without a runnable mental model falls apart on the whiteboard.
The three states
A promise is always in exactly one state:
- pending: not settled yet
- fulfilled: settled with a value
- rejected: settled with a reason (usually an Error)
Settlement is final. A fulfilled promise does not later reject. A rejected promise does not later fulfill. That immutability is why chains are safe to share and why race conditions on "who settled it" are not the usual interview trap. The trap is what you schedule after settlement.
then, catch, and finally
then registers what to do when a promise fulfills (and optionally when it rejects). catch is sugar for then(undefined, onRejected). finally runs after settlement for cleanup, and by default passes the outcome through. If finally throws, or returns a rejecting promise, that overrides the prior outcome.
Important interview detail: then and catch return a new promise. They do not "add a listener and return the same object." That new promise settles based on what your callback returns or throws.
Promise.resolve(1)
.then((n) => n + 1)
.then((n) => {
console.log(n); // 2
return n * 10;
})
.then((n) => console.log(n)); // 20If a then callback returns a value, the next promise fulfills with that value. If it throws, the next promise rejects with that error. If it returns another promise, the chain adopts that promise's settlement. That "return a promise" rule is how you flatten nested async work.
Errors travel down the chain
Rejections skip fulfilled handlers until they hit a rejection handler.
Promise.resolve()
.then(() => {
throw new Error("boom");
})
.then(() => {
console.log("skipped");
})
.catch((err) => {
console.log(err.message); // boom
})
.then(() => {
console.log("continues after catch");
});After a catch that handles the error and returns normally, the chain is fulfilled again. That is why "catch then keep going" is a feature, and why an empty catch can accidentally hide bugs.
Unhandled rejection: if a promise rejects and nothing in the chain (and no later catch) handles it, the host reports an unhandled promise rejection. In interviews, say you always either return the promise to a caller that will catch, or attach a catch at the boundary you own.
async/await is promise sugar
An async function always returns a promise. The body runs synchronously until the first await. await pauses the async function and schedules the continuation as a microtask once the awaited promise settles. On fulfillment, the continuation resumes with the value. On rejection, it resumes by throwing that reason, which you can catch with try/catch. It does not block the JavaScript thread.
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error("failed");
return res.json();
}
// Equivalent idea with then:
function loadUserThen(id) {
return fetch(`/api/users/${id}`).then((res) => {
if (!res.ok) throw new Error("failed");
return res.json();
});
}try/catch around await catches rejections from awaited promises in that async function, the same way catch does on a chain. Prefer async/await when the control flow is sequential and branching. Prefer explicit then when you are composing many pipelines or returning early from helpers that should stay thenable.
Classic gotcha: forgetting return
This is one of the most common whiteboard fails.
// Broken: inner promise is started, but the outer chain does not wait for it
function broken() {
return fetch("/api/a").then((a) => {
fetch("/api/b"); // missing return
});
}
// Fixed
function fixed() {
return fetch("/api/a").then((a) => {
return fetch("/api/b");
});
}Without return, the next then runs with undefined as soon as the first then callback finishes, while /api/b may still be in flight. In async/await the equivalent bug is starting a promise and never awaiting it.
Classic gotcha: sequential await vs Promise.all
// Serial: each request waits for the previous one
const a = await fetch("/api/a").then((r) => r.json());
const b = await fetch("/api/b").then((r) => r.json());
// Parallel: start both, then wait together
const [a2, b2] = await Promise.all([
fetch("/api/a").then((r) => r.json()),
fetch("/api/b").then((r) => r.json()),
]);Promise.all fulfills when every input fulfills, with results in input order. It rejects when any input rejects (fail-fast). Promise.allSettled waits for every settlement and never "fails the batch" early. Promise.race settles with the first settlement, win or lose.
Interview signal: if the tasks are independent, parallelize. If task B needs A's result, sequence. Saying "I always await in a for-loop" without noticing independence costs latency.
Promise constructor: when you need it
Most application code should return or await existing promises (fetch, ORM calls, timers you already promisified) rather than new Promise. Use the constructor when you adapt a callback API.
function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}The executor function runs synchronously when you call new Promise. resolve and reject settle the promise. Calling them more than once is a no-op after the first settlement. Throwing inside the executor rejects the promise.
What to say out loud in an interview
A strong answer sounds like this:
- A promise is a state machine: pending, then fulfilled or rejected, once.
- then/catch/finally schedule microtask reactions and return new promises, so chains compose.
- Returned values fulfill the next link; throws reject it; returned promises are adopted.
async/awaitis sugar: async returns a promise; await waits for settlement (value on fulfill, throw on reject) and continues as a microtask; the thread is not blocked.- Watch for forgotten returns, empty catches, and serial awaits that should be
Promise.all.
Then walk one small demo: a throw inside then, a catch that recovers, and a parallel Promise.all vs two awaits.
Common wrong answers to avoid
- "Promises make JavaScript multi-threaded." No. Reactions still run on the one JS thread as microtasks.
- "await blocks the browser until the network returns." No. It yields. Other sync code and other tasks keep going.
- "then mutates the same promise." No. It returns a new promise.
- "catch ends the chain forever." No. A catch that handles and returns resumes as fulfilled.
- "
Promise.allwaits for all even after a failure." That isallSettled. all rejects on the first rejection. - "I need
new Promisefor every async call." Prefer returning or awaiting existing promises; use the constructor to adapt callback APIs.
Takeaways
- Promises model future values with a one-way state machine.
- Chains compose through returned promises, values, and throws.
async/awaitis readable sugar over the same microtask model.- Return what you want the next step to wait for. Await what you need before continuing.
- Parallelize independent work with
Promise.all; sequence dependent work on purpose.
If you can narrate states, chaining, error recovery, and all vs sequential await with a tiny demo, you are interview-ready on promises.