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;