Mastering Async/Await in Node.js: A Guide
Stop writing .then() chains! Learn how to write clean, synchronous-looking asynchronous code using modern JavaScript features supported by the WebFiddle engine.
The History of Asynchrony#
JavaScript is single-threaded. This means it can only do one thing at a time. However, modern apps need to:
- Read a file.
- Query a database.
- Fetch from an external API.
- Send an email.
If Node.js paused the entire server while waiting for the email to send, your site would be incredibly slow.
The Dark Ages: Callbacks
In 2010, we handled this with callbacks.
getUser(function(user) {
getPosts(user.id, function(posts) {
getComments(posts[0].id, function(comments) {
// "Callback Hell" or "The Pyramid of Doom"
});
});
});
The Transition: Promises
ES6 introduced Promises, which allowed chaining .then(). Better, but still verbose.
The Solution: Async/Await
Introduced in ES2017 (Node 8+), async/await is just "syntactic sugar" over Promises, but it makes asynchronous code look like modern, synchronous code.
1. Sequential Execution#
The most basic pattern. We wait for one thing to finish before starting the next.
async function processUser(userId) {
try {
console.log('Fetching user...');
const user = await db.getUser(userId); // Pauses here!
console.log('Fetching posts...');
const posts = await db.getPosts(user.id); // Pauses here!
return posts;
} catch (error) {
console.error('Something went wrong:', error);
}
}
Pros: Easy to read.
Cons: Slow if the tasks are independent. If getUser takes 1s and getPosts takes 1s, total time is 2s.
2. Parallel Execution (The Performance Boost)#
Often, beginners overuse await. If two tasks don't depend on each other, don't await them one by one!
async function getDashboardData() {
// Start both requests immediately!
const userPromise = db.getUser();
const statsPromise = db.getStats();
// Wait for BOTH to finish
const [user, stats] = await Promise.all([userPromise, statsPromise]);
return { user, stats };
}
Pros: Faster! If getUser takes 1s and getStats takes 1s, total time is roughly 1s (because they run at the same time).
3. Handling Errors with Promise.allSettled#
A danger of Promise.all is that if one request fails, everything crashes.
In Node.js 12.9+, we got Promise.allSettled.
const results = await Promise.allSettled([
api.criticalCall(),
api.optionalCall()
]);
// result[0] might be { status: 'fulfilled', value: ... }
// result[1] might be { status: 'rejected', reason: ... }
This is perfect for dashboards where you want to show "User Profile" even if the "News Feed" API failed.
Top-Level Await#
In modern Node.js (modules) and here in WebFiddle, you can use await outside of functions.
// index.mjs
const response = await fetch('https://api.github.com/zen');
const txt = await response.text();
console.log(txt);
Summary#
- Use
async/awaitfor cleaner code. - Use
try/catchfor error handling (no more.catch()chains). - Use
Promise.all()when tasks are independent to speed up your app. - Practice these patterns in the WebFiddle Node.js Sandbox!
WebFiddle