13. Callbacks and Promises: what actually happens under the hood
Before Promises, async results were delivered via callbacks — functions passed in and invoked later. Nesting them for sequential async steps produces "callback hell":
getUser(id, (err, user) => {
if (err) return handleError(err);
getPosts(user.id, (err, posts) => {
if (err) return handleError(err);
getComments(posts[0].id, (err, comments) => {
if (err) return handleError(err);
render(user, posts, comments); // 3 levels deep, error handling repeated 3x
});
});
});
A Promise is a state machine with exactly three states — pending →
fulfilled or pending → rejected — and once settled, it never changes
again. .then() doesn't run the callback synchronously even if the promise
is already resolved; it always schedules it as a microtask, which is what
guarantees consistent ordering.
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
delay(100, "done")
.then((value) => { console.log(value); return value.toUpperCase(); })
.then((upper) => console.log(upper))
.catch((err) => console.error("caught:", err)); // catches a rejection at ANY step above
Promise combinators for concurrent work:
Promise.all([p1, p2, p3]); // resolves when ALL resolve; rejects fast on the FIRST rejection
Promise.allSettled([p1, p2, p3]); // always resolves, with { status, value|reason } for each
Promise.race([p1, p2, p3]); // settles as soon as the FIRST one settles (resolve or reject)
Promise.any([p1, p2, p3]); // resolves on the FIRST fulfillment; rejects only if ALL reject
War story. A team used
Promise.allto fetch 5 independent dashboard widgets' data. One flaky endpoint failing 2% of the time took down the entire dashboard on every request, becausePromise.allrejects as soon as any one promise rejects — the other 4 successful results were thrown away. Switching toPromise.allSettledand rendering each widget independently based on its own settled result fixed it in one line.