How to Use Async/Await in Node.js
This article explains how to write clean, non-blocking asynchronous
code in Node.js using async/await. You will learn how to
transition from nested callbacks and promises to cleaner syntax,
implement robust error handling, and run asynchronous operations in
parallel to keep your applications fast and responsive.
Understanding Async/Await
In Node.js, asynchronous operations prevent the single-threaded event
loop from blocking. While callbacks and Promises solved this
historically, they often led to complex, hard-to-read code (known as
“callback hell”). Introduced in ES2017, async/await is
syntactic sugar built on top of Promises. It allows you to write
asynchronous code that reads like synchronous code, making it highly
readable and easier to maintain.
To use it, you define a function with the async keyword.
Inside this function, you use the await keyword before any
expression that returns a Promise. The execution of the function pauses
at the await line until the Promise resolves or
rejects.
Basic Syntax Example
Here is a simple comparison showing how async/await
simplifies Promise chains.
Using Promises:
const fs = require('fs').promises;
function getUserData() {
fs.readFile('user.json', 'utf-8')
.then(data => {
console.log(JSON.parse(data));
})
.catch(err => {
console.error(err);
});
}Using Async/Await:
const fs = require('fs').promises;
async function getUserData() {
const data = await fs.readFile('user.json', 'utf-8');
console.log(JSON.parse(data));
}Robust Error Handling
One of the greatest advantages of async/await is that
you can handle both synchronous and asynchronous errors using standard
try...catch blocks. This unifies your error-handling
logic.
async function fetchUser(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const user = await response.json();
return user;
} catch (error) {
console.error('Failed to fetch user:', error.message);
// Handle error or rethrow
}
}Keeping Code Non-Blocking with Parallel Execution
A common mistake when using async/await is executing
independent operations sequentially, which unnecessarily blocks the
execution flow.
For example, if you need to fetch a user’s profile and their post history, executing them sequentially doubles the waiting time:
// Slow: Sequential execution (blocking subsequent calls)
async function getProfileData(userId) {
const profile = await fetchProfile(userId); // Waits for this to finish...
const posts = await fetchPosts(userId); // ...before starting this
return { profile, posts };
}Because fetching the profile and fetching the posts do not depend on
each other, you should run them concurrently using
Promise.all(). This triggers both requests simultaneously
and waits for both to resolve:
// Fast: Concurrent execution (non-blocking)
async function getProfileData(userId) {
try {
const [profile, posts] = await Promise.all([
fetchProfile(userId),
fetchPosts(userId)
]);
return { profile, posts };
} catch (error) {
console.error('Error fetching data', error);
}
}By using Promise.all(), you maximize the efficiency of
the Node.js event loop, ensuring your asynchronous code remains clean,
readable, and highly performant.