A Promise is an object representing a value that will exist later. It’s in one of three states: pending, then exactly once it becomes fulfilled (with a value) or rejected (with a reason). Once settled, it never changes again.
Two terms worth saying precisely: a promise is settled once it is either fulfilled or rejected (no longer pending). “Resolved” is slightly different — a resolved promise has been locked to follow another promise/thenable, which may still be pending. In practice “resolved” ≈ “will fulfil/reject based on something else.” The constructor gives you resolve and reject:
console.log. Async logs (setTimeout/Promise) appear in real execution order.Chaining and flattening
.then returns a new promise, so calls chain. Crucially, if a .then callback returns a promise, the chain waits for it — promises auto-flatten, which is what makes sequential async readable.
fetchUser(id)
.then(user => fetchPosts(user.id)) // returns a promise → chain waits
.then(posts => render(posts))
.catch(err => showError(err)) // one catch handles any earlier rejection
.finally(() => stopSpinner()); // runs regardless
A single .catch at the end handles a rejection from any preceding step, because a rejection skips .then handlers until it finds a .catch. .catch(fn) is just sugar for .then(undefined, fn).
Flattening rule: what a .then callback returns decides the next promise’s value. Return a plain value → next .then gets that value. Return a promise → the chain waits for it and unwraps it (no nested Promise<Promise<T>>). Throw → the chain rejects.
console.log. Async logs (setTimeout/Promise) appear in real execution order.async/await is the same thing, nicer
async functions always return a promise. await pauses the function until the awaited promise settles, then resumes with its value (or throws its rejection). With await, you use ordinary try/catch:
async function load(id) {
try {
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
render(posts);
} catch (err) {
showError(err);
} finally {
stopSpinner();
}
}
await suspends only the async function, not the whole program. The engine is free to run other tasks meanwhile; when the promise settles, the rest of the function is scheduled as a microtask. (See the Event Loop lesson — everything after an await is a microtask continuation.)Series vs parallel — the most common mistake
Awaiting in a loop runs requests one at a time. If they’re independent, that’s needlessly slow. Kick them all off, then await together:
console.log. Async logs (setTimeout/Promise) appear in real execution order.The concurrency combinators
| Helper | Resolves when | Rejects when | Use for |
|---|---|---|---|
Promise.all | all fulfil | any rejects (fail-fast) | need every result |
Promise.allSettled | all settle | never | want each outcome regardless |
Promise.race | first settles | first settles (if reject) | timeouts |
Promise.any | first fulfils | all reject (AggregateError) | first success wins |
Promise.all — fail-fast; one rejection rejects the whole thing (other promises still run, but their results are discarded). Use when you need every result and any failure is fatal.
console.log. Async logs (setTimeout/Promise) appear in real execution order.Note the result array preserves input order, not settle order.
Promise.allSettled — never rejects; you get {status, value} or {status, reason} for each. Use when you want partial success (e.g. fetch 10 widgets, render the 8 that worked).
console.log. Async logs (setTimeout/Promise) appear in real execution order.Promise.race — settles with the first to settle, success or failure. Classic use: a timeout wrapper.
console.log. Async logs (setTimeout/Promise) appear in real execution order.Promise.any — resolves with the first fulfilment, ignoring rejections; rejects only if all reject (with an AggregateError). Use for “first success wins” (e.g. fastest of several mirrors).
console.log. Async logs (setTimeout/Promise) appear in real execution order.Converting callbacks to promises
Legacy APIs use (err, result) callbacks. Wrap them once in a promise (this is what Node’s util.promisify does):
console.log. Async logs (setTimeout/Promise) appear in real execution order.What’s the output? (timing puzzle)
console.log. Async logs (setTimeout/Promise) appear in real execution order.Answer: 1, 4, 6, 3, 5, 2. Sync: 1, then the async IIFE runs up to await printing 4, then 6. Microtasks: 3 (the .then) and 5 (the await continuation) in scheduling order. Macrotask 2 last.
Error-handling pitfalls
- Forgotten
.catch— an unhandled rejection triggersunhandledrejection(browser) / crashes the process (Node, by default in recent versions). try/catchwon’t catch un-awaited promises —try { doAsync(); } catch {}catches nothing; you mustawaitinside thetry.- Rejection inside a non-async callback — e.g. throwing inside a
setTimeoutcallback isn’t caught by an outertry/catch. Promise.allswallows later results on first reject — useallSettledif you need every outcome.returnvs noreturnin a.then— forgetting to return a promise breaks the chain’s waiting and ordering.asyncfunction throwing rejects its returned promise; the caller must.catch/await-in-try.
Common gotchas
- Sequential awaits for independent work is the #1 perf bug — parallelize with
Promise.all. awaitin aforEachdoes nothing useful —forEachignores the returned promise; use afor...ofloop (sequential) ormap+Promise.all(parallel).- Mixing
.thenandawaitunnecessarily — pick one style per function for readability. - Creating promises that never settle (forgetting to call resolve/reject) leaks pending state.
Interview questions & model answers
Q: What are the promise states? Pending, fulfilled, rejected. A promise settles once (fulfilled or rejected) and is immutable thereafter.
Q: Series vs parallel — how do you parallelize?
Start all promises first, then await Promise.all([...]). Awaiting one-by-one in a loop serializes independent work.
Q: all vs allSettled vs race vs any?
all: all-or-nothing, fail-fast. allSettled: every outcome, never rejects. race: first to settle (success or failure). any: first fulfilment, rejects only if all fail.
Q: Is await blocking?
It suspends only its async function and yields to the event loop; the continuation runs as a microtask. The thread isn’t blocked.
Q: How do error handling styles differ?
.then/.catch chains rejections to a trailing .catch; async/await uses ordinary try/catch. Both end up handling the same rejections.
Q: How do you convert a callback API to a promise?
Wrap it in new Promise((resolve, reject) => ...), calling resolve/reject from the callback — or use util.promisify in Node.
Q: What happens to an unhandled rejection?
It fires unhandledrejection in browsers and, in modern Node, terminates the process by default. Always attach a .catch or wrap awaits in try/catch.
Say it out loud (30s)
“A promise is a future value in pending/fulfilled/rejected.
.thenreturns a new promise and auto-flattens returned promises, so chains stay flat; one trailing.catchhandles any rejection.async/awaitis sugar over this with normal try/catch.awaitsuspends only its function — the continuation runs as a microtask — so to parallelize independent work I start all the promises andPromise.allthem rather than awaiting in a loop.”