Evolution of Async Patterns
MedJavaScript is single-threaded but handles async operations elegantly. From callbacks to async/await, each pattern solved problems while creating new ones. Understanding this evolution helps you write better async code.
Interactive Visualization
1
Callbacks
1995-2011Callbacks were the original async pattern. Pass a function to be called later.
setTimeoutXMLHttpRequestNode.js
Example Code
// The original async pattern: callbacks
function fetchUser(id, callback) {
setTimeout(() => {
const user = { id, name: 'Alice' };
callback(null, user); // (error, result) convention
}, 1000);
}
// Usage
fetchUser(1, function(err, user) {
if (err) {
console.error('Failed:', err);
return;
}
console.log('Got user:', user.name);
});
// Node.js made this the standard pattern
// Error-first callbacks: callback(err, result)Problems Solved
- Enabled async operations
- Simple mental model
- Works everywhere (no polyfills)
New Challenges
- Inversion of control (who calls callback?)
- No guaranteed single call
- Hard to compose multiple async ops
These challenges led to: Callback Hell
1 / 6
Key Insight: Async/await won because it makes async code look synchronous while preserving all Promise capabilities.
Key Points
- Callbacks (1995): Pass a function to run later - simple but leads to nesting
- Callback Hell (2008): Deep nesting creates unreadable "pyramid of doom"
- Promises (2012): Chainable .then() flattens code, unified error handling
- Generators (2015): Pausable functions, paved the way for async/await
- Async/Await (2017): Sync-looking code, native try/catch, clear winner
- Modern Patterns: Promise.allSettled, AbortController, async iterators
Code Examples
Era 1: Callbacks
// Pass a function to be called later function fetchUser(id, callback) { setTimeout(() => { callback(null, { id, name: 'Alice' }); }, 1000); } fetchUser(1, function(err, user) { if (err) return console.error(err); console.log(user.name); });
Callbacks are the original async pattern. Simple, but hard to compose.
Era 2: Callback Hell
// The "Pyramid of Doom" getUser(id, function(err, user) { getOrders(user.id, function(err, orders) { getDetails(orders[0].id, function(err, details) { // 3 levels deep... keeps going }); }); });
Sequential async ops create deeply nested, unreadable code.
Era 3: Promises
// Chain instead of nest fetchUser(1) .then(user => getOrders(user.id)) .then(orders => getDetails(orders[0].id)) .catch(err => console.error(err)); // Parallel execution Promise.all([fetchA(), fetchB(), fetchC()]) .then(([a, b, c]) => console.log(a, b, c));
Promises flatten the pyramid with .then() chains.
Era 4: Async/Await
async function loadOrder(userId) { try { const user = await fetchUser(userId); const orders = await getOrders(user.id); const details = await getDetails(orders[0].id); return { user, orders, details }; } catch (err) { console.error('Failed:', err); } }
Async/await makes async code look synchronous. The clear winner.
Modern: Promise.allSettled
// Don't fail on first error const results = await Promise.allSettled([ fetchUser(1), // succeeds fetchUser(-1), // fails fetchUser(2) // succeeds ]); // All results returned, even failures
Promise.allSettled handles partial failures gracefully.
Modern: AbortController
const controller = new AbortController(); fetch('/api/data', { signal: controller.signal }) .then(r => r.json()) .catch(err => { if (err.name === 'AbortError') { console.log('Fetch cancelled'); } }); // Cancel the request controller.abort();
AbortController lets you cancel in-flight async operations.
Common Mistakes
- Forgetting await (Promise returned instead of value)
- Using await in a loop when Promise.all would parallelize
- Not handling Promise rejections (unhandled rejection)
- Mixing .then() and await inconsistently
Interview Tips
- Explain why async/await is just syntax sugar over Promises
- Know when to use Promise.all vs Promise.allSettled vs Promise.race
- Understand the event loop and how async fits in
- Be ready to convert callback code to Promises to async/await
- Know how to handle errors in async/await (try/catch vs .catch())