Promises
Prerequisites assumed: function scope, arrow functions, the callback pattern from "Asynchrony with Callbacks," and a working mental model of the event loop's task queue.
The core model
A Promise is a placeholder object for a value that will arrive later. It is a state machine with exactly three states: pending, fulfilled (with a value), or rejected (with a reason, usually an Error). Transitions are one-way and happen at most once. Multiple .then handlers attached to the same promise all observe the same outcome; there is no "reuse" problem like the ad-hoc callbacks of Node-style APIs.
The key insight: .then does not wait—it schedules. Every handler runs as a microtask after the current synchronous execution completes, and each .then returns a new promise, forming a chain:
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
wait(500)
.then(() => { throw new Error("boom"); })
.catch((err) => console.error("caught:", err.message))
.then(() => console.log("runs after recovery"));Rejection propagates down the chain until a .catch handles it; a fulfilled value propagates until a .then transforms it.
Gotchas and trade-offs
- Mixing construction and callbacks. Calling
resolveinside a legacy callback style is fine, but forgettingrejecton the error path yields a promise that hangs forever. Never construct a promise whose executor can throw after resolution—extraresolve/rejectcalls are silently ignored, which masks bugs. - Swallowed rejections. A
.thenwithout a rejection handler creates a new promise that rejects; failing to attach a.catchanywhere in the chain surfaces only as an unhandled rejection warning. Node may crash on these in future versions. - Non-promise values in chains.
Promise.resolve(v).then(f)still defersfto a microtask;f(v)directly does not. This ordering difference matters when interleaving with I/O. - Combinators.
Promise.allfails fast on the first rejection;Promise.allSettlednever rejects;Promise.anyresolves with the first fulfillment;Promise.racesettles with whichever settles first. Choose by failure semantics, not convenience.
const results = await Promise.allSettled([fetch("/a"), fetch("/b")]);
const failed = results.filter((r) => r.status === "rejected");When promises are the wrong tool
Streams, backpressure, and cancellation exceed what a single-shot promise expresses. AbortController bolted on top works, but if you need multi-value or cancellable semantics, prefer async iterables over contorted promise chains.
Exercise
Why does a .then handler attached to an already-fulfilled promise still run asynchronously rather than immediately?
Show answer
Because handlers are always queued as microtasks when their promise settles, guaranteeing uniform, predictable ordering regardless of when the promise resolved—never synchronously reentrant.
Exercise
You need every result, including failures. Which combinator and why?
Show answer
Promise.allSettled—it never rejects and reports each input as fulfilled or rejected, unlike Promise.all, which discards remaining work on the first failure.
Spotted a mistake, or code that does not work?