DevPrep
  • Interview Prep
  • Projects
  • Resources
  • Pricing
  • About Us
Submit Question
DevPrep
  • Pricing
  • About Us
Submit Question
  1. Home
  2. Articles
  3. Frontend Engineering
  4. JavaScript Async Patterns: Callbacks to Async/Await Evolution
XLinkedInReddit
MediumFrontend Engineering

JavaScript Async Patterns: Callbacks to Async/Await Evolution

D
DevPrep Team
February 10, 2026·2 min read·0
Table of Contents
  • Era 1: Callbacks (2009)
  • Era 2: Promises (2015 - ES6)
  • Era 3: Async/Await (2017 - ES8)
  • Common Async/Await Mistakes
  • Sequential When Parallel Is Possible
  • Forgetting Error Handling
  • Async in forEach
  • Top-Level Await

Understanding how JavaScript's async model evolved helps you appreciate why we write code the way we do today.

Era 1: Callbacks (2009)

// Callback Hell / Pyramid of Doom
getUser(userId, (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);
      renderPage(user, posts, comments);
    });
  });
});

Problems: Deep nesting, error handling at every level, impossible to reason about flow.

Era 2: Promises (2015 - ES6)

getUser(userId)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => renderPage(comments))
  .catch(handleError); // Single error handler!

Improvement: Flat chain, centralized error handling. But still not intuitive for complex flows.

Era 3: Async/Await (2017 - ES8)

async function loadPage(userId) {
  try {
    const user = await getUser(userId);
    const posts = await getPosts(user.id);
    const comments = await getComments(posts[0].id);
    renderPage(user, posts, comments);
  } catch (error) {
    handleError(error);
  }
}

Reads like synchronous code. Error handling with try/catch. Easy to debug with breakpoints.

Common Async/Await Mistakes

Sequential When Parallel Is Possible

// Bad: sequential (3 seconds)
const users = await fetchUsers();     // 1s
const posts = await fetchPosts();     // 1s
const comments = await fetchComments(); // 1s

// Good: parallel (1 second)
const [users, posts, comments] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
  fetchComments()
]);

Forgetting Error Handling

// Bad: unhandled rejection
const data = await fetchData(); // If this fails, crash!

// Good
try {
  const data = await fetchData();
} catch (error) {
  showFallback();
}

Async in forEach

// Bad: doesn't wait for completion
items.forEach(async (item) => {
  await processItem(item); // These run in parallel, not sequential!
});

// Good: sequential
for (const item of items) {
  await processItem(item);
}

// Good: parallel
await Promise.all(items.map(item => processItem(item)));

Top-Level Await

ES2022 allows await at the module top level — no need for async IIFE wrappers.

// module.mjs
const config = await loadConfig();
export default config;

Related Articles

MediumFrontend Engineering

System Design #12: Design a Multi-Step Form Wizard

7 min read
MediumFrontend Engineering

Mastering Senior-Level JavaScript Interview Concepts

2 min read
MediumFrontend Engineering

System Design #9: Design a Collaborative Text Editor

9 min read

Comments (0)

Sign in to leave a comment.

No comments yet. Be the first to comment.

Table of Contents

  • Era 1: Callbacks (2009)
  • Era 2: Promises (2015 - ES6)
  • Era 3: Async/Await (2017 - ES8)
  • Common Async/Await Mistakes
  • Sequential When Parallel Is Possible
  • Forgetting Error Handling
  • Async in forEach
  • Top-Level Await

Series

View all Frontend Engineering articles →

Practice

  • JavaScript
  • DSA
  • Machine Coding
  • System Design

Resources

  • Learning Tracks
  • Articles
  • Roadmaps
  • Compare Concepts
  • Glossary
  • Developer Tools
  • All Questions

Company

  • About
  • Pricing

Legal

  • Privacy Policy
  • Terms of Service
DevPrep

© 2026 DevPrep. All rights reserved.