Evolution of Async Patterns

Med

JavaScript 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-2011

Callbacks 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())