Async Patterns: Callbacks, Promises, Async/Await
Async Patterns: Callbacks, Promises, and Async/Await
The evolutionary journey of asynchronous JavaScript from Callback Hell to synchronous-style concurrency.
The Evolution of Postal Orders
JavaScript async patterns evolved to tame inversion of control, nested pyramids of doom, and error bubbling across asynchronous boundary hops.
1. The Callback Era & Inversion of Control
Passing callbacks gave control over when and how many times our code executed to third-party libraries, creating the "Pyramid of Doom".
2. The Promise Revolution
Promises un-inverted control by returning a trusted event listener proxy. Promises guarantee: exactly one settlement, immutability, and unified `.catch()` error handling.
3. Async/Await (Syntactic Sugar)
`async` functions always wrap returns in a Promise. `await` pauses execution until the promise settles, unwrapping its resolution value or throwing its rejection.
4. Concurrent Execution Patterns
Running multiple independent promises concurrently with `Promise.all()` avoids sequential performance bottlenecks (waterfalls).
Eliminating the Sequential Waterfall Trap using Promise.all:
const fetchProduct = (id) => new Promise(res => setTimeout(() => res(`Product ${id}`), 50));
const fetchReviews = (id) => new Promise(res => setTimeout(() => res(`Reviews for ${id}`), 50));
// Slow Sequential Waterfall (takes 50ms + 50ms = 100ms)
async function getDetailsSlow(id) {
const product = await fetchProduct(id);
const reviews = await fetchReviews(id);
return { product, reviews };
}
// Fast Concurrent Execution (takes max(50ms, 50ms) = 50ms)
async function getDetailsFast(id) {
const [product, reviews] = await Promise.all([
fetchProduct(id),
fetchReviews(id)
]);
return { product, reviews };
}
getDetailsFast(42).then(data => console.log("Concurrent result:", data));
Concurrent result: { product: 'Product 42', reviews: 'Reviews for 42' }
The Sequential Waterfall Trap
The Risk: Writing `await callA(); await callB();` when `callB` does not depend on `callA` needlessly serializes independent network requests, doubling response latency.
The Fix: Fire independent promises concurrently: `await Promise.all([callA(), callB()])`.
Forgetting await in try/catch
The Risk: Returning an un-awaited promise inside a try block (`try { return fetch(); } catch (e) {}`) bypasses the catch block on rejection.
The Fix: Use `return await fetch();` if you need the local try/catch block to catch errors before returning.
- ✓ Async/await is built on top of Promises and Generators.
- ✓ `async` functions always return a Promise.
- ✓ `await` pauses local function execution without blocking the main JS thread.
- ✓ Use `Promise.all()` to run independent promises concurrently.
- ✓ Wrap `await` in standard `try...catch...finally` for clean error recovery.