Asynchronous JavaScript: Callbacks, Promises & async/await
From callback hell to elegant async/await: how JavaScript models operations that finish later, how promises chain and fail, and how to run work in parallel safely.
4 sections · ~30 min · 5-question quiz (pass ≥ 70%)
1Why Async? From Callbacks to Callback Hell
JavaScript runs on one thread — so slow operations (network, disk, timers) must not block it. The original answer was callbacks: "here's a function, call it when you're done."
loadUser(id, (err, user) => {
if (err) return handle(err);
loadOrders(user, (err, orders) => {
if (err) return handle(err);
loadDetails(orders[0], (err, details) => {
if (err) return handle(err);
render(details); // welcome to the pyramid of doom
});
});
});
Problems: deep nesting, error handling repeated at every level, and no way to compose or combine operations. Promises were designed to fix exactly this.
2Promises: Values From the Future
A Promise is an object representing an eventual result. It's in exactly one of three states: pending → fulfilled (with a value) or rejected (with a reason). Once settled, it never changes.
fetchUser(id)
.then(user => fetchOrders(user)) // return a promise → chain waits for it
.then(orders => render(orders))
.catch(err => showError(err)) // ONE handler catches any failure above
.finally(() => hideSpinner()); // runs either way
Key mechanics:
.thenreturns a new promise, which is what makes flat chaining possible.- Returning a promise inside
.theninserts it into the chain. - A rejection skips forward to the nearest
.catch— like a thrown exception falling through to a try/catch. .catchreturns a normal promise too, so the chain can recover and continue.
3async/await: Synchronous-Looking Async
async/await is syntax over promises — the same machinery, dramatically more readable:
async function showOrders(id) {
try {
const user = await fetchUser(id); // pauses THIS function, not the thread
const orders = await fetchOrders(user);
render(orders);
} catch (err) { // normal try/catch works!
showError(err);
}
}
Rules that matter:
- An
asyncfunction always returns a promise —return 42fulfills it with 42;throwrejects it. awaitunwraps a promise's value, or re-throws its rejection.- While awaiting, the function is suspended and the event loop keeps running everything else —
awaitnever blocks the thread. - Forgetting
awaitis a classic bug: you get a Promise object instead of its value, and errors vanish into an unhandled rejection.
4Running Things in Parallel (and Other Combinators)
Sequential awaits are a hidden performance trap:
// ❌ Sequential — 600ms total if each takes 300ms
const a = await fetchA();
const b = await fetchB();
// ✅ Parallel — ~300ms: start both, then wait for both
const [a2, b2] = await Promise.all([fetchA(), fetchB()]);
The four combinators:
Promise.all(promises)— fulfills with an array of results; rejects fast on the first failure.Promise.allSettled(promises)— never rejects; gives{status, value|reason}for each. Use when partial failure is OK.Promise.race(promises)— settles as soon as any settles (win or lose). Classic use: timeouts.Promise.any(promises)— fulfills with the first success, rejects only if all fail.
And remember from the event loop: promise callbacks are microtasks — they run before any pending setTimeout macrotask.