Promises
A Promise is an object that represents a value you do not have yet — the eventual result of an asynchronous operation. Instead of passing a callback to be called later, the async function returns a promise, and you attach handlers to it with .then, .catch and .finally. Promises turn nested callbacks into chains that read top-to-bottom.
The three states
A promise is always in one of these states, and only transitions once:
Pending — the operation is in progress. The value is not known yet.
Fulfilled — the operation succeeded with a value.
Rejected — the operation failed with a reason (usually an
Error).
Once a promise is fulfilled or rejected, it is settled — its state never changes again, and its value/reason is fixed forever.
Creating a promise
Most code consumes promises returned by libraries (fetch, fs/promises, etc.) rather than creating them. But you create one with the Promise constructor when wrapping a callback-based API.
const wait = (ms) =>
new Promise((resolve, reject) => {
if (ms < 0) {
reject(new Error("ms must be non-negative"));
return;
}
setTimeout(() => resolve("done"), ms);
});
wait(500).then((result) => console.log(result)); // "done", after 500msPromise.resolve and Promise.reject
Two static shortcuts for creating already-settled promises. Useful for tests and for "lifting" a sync value into a promise chain.
Promise.resolve(42).then((v) => console.log(v)); // 42
Promise.reject(new Error("nope"))
.catch((err) => console.log(err.message)); // "nope"
// Passing an existing promise to Promise.resolve returns it unchanged.
const p = fetch("/api");
console.log(Promise.resolve(p) === p); // true.then, .catch, .finally
.then takes up to two functions: one for fulfilled, one for rejected. In practice most code passes only the fulfilled handler and uses .catch for errors — it reads better.
fetch("/api/user")
.then((response) => response.json())
.then((user) => console.log(user))
.catch((err) => console.error("failed:", err))
.finally(() => console.log("done either way"));.then(onFulfilled, onRejected)— handle either outcome..catch(onRejected)— shorthand for.then(undefined, onRejected)..finally(onSettled)— run cleanup regardless of outcome. The value/error passes through.
Chaining: every handler returns a new promise
The big insight: .then returns a new promise whose value is whatever its handler returned. If the handler returns a promise, the chain waits for it. That is what makes long pipelines flat.
Promise.resolve(2) .then((n) => n + 1) // 3 .then((n) => Promise.resolve(n * 10)) // waits, then 30 .then((n) => console.log(n)); // 30
Compare to the old "pyramid of doom":
Callback version
getUser(id, (err, user) => {
if (err) return done(err);
getPosts(user.id, (err, posts) => {
if (err) return done(err);
renderPosts(posts, (err) => {
if (err) return done(err);
done(null);
});
});
});Promise version
getUser(id) .then((user) => getPosts(user.id)) .then((posts) => renderPosts(posts)) .catch((err) => done(err));
Error propagation
A rejection skips every .then until it hits a .catch. Inside a .then handler, throwing is equivalent to returning a rejected promise.
Promise.resolve("input")
.then((s) => {
if (s.length < 5) throw new Error("too short");
return s.toUpperCase();
})
.then((s) => console.log("got", s))
.catch((err) => console.log("caught:", err.message));caught: too short
Common pitfalls
Forgetting to return inside
.then. If you do not return, the next handler receivesundefinedand the chain does not wait for nested work.Catch in the wrong place. A
.catchonly handles rejections from upstream steps. Put one at the end of every chain to be safe.Unhandled rejection. A rejection with no
.catchtriggers anunhandledrejectionevent in browsers (and crashes Node by default).Constructor anti-pattern. Wrapping an existing promise in a new one — see above.
Forgotten return
function loadUser(id) {
return Promise.resolve({ id, name: "Ada" });
}
// BUG: loadUser is fired, but its promise is dropped.
function badChain(id) {
return Promise.resolve()
.then(() => {
loadUser(id); // missing return
})
.then((user) => console.log(user)); // user is undefined
}
// FIX: return the inner promise.
function goodChain(id) {
return Promise.resolve()
.then(() => loadUser(id))
.then((user) => console.log(user));
}Promises and microtasks
.then handlers run as microtasks — sooner than setTimeout(fn, 0), but never synchronously. Even Promise.resolve(1).then(...) is not synchronous: the handler is scheduled, not called immediately.
console.log("a");
Promise.resolve().then(() => console.log("b"));
console.log("c");a c b