While Promises (with .then() and .catch()) are a huge improvement over callbacks, they can still become slightly difficult to read when dealing with multiple interconnected asynchronous operations.
Enter Async/Await. Introduced in modern JavaScript (ES8), async/await is "syntactic sugar" built on top of Promises. It allows you to write asynchronous, non-blocking code that looks and feels like traditional synchronous code.
async KeywordTo use async/await, you must first declare a function with the async keyword.
When you place async before a function, it does two things:
await keyword inside that function.
// Declaring an async function
async function greetUser() {
return "Hello, Node.js Developer!";
}
// Because it's async, it returns a Promise. We can use .then()
greetUser().then(message => {
console.log(message); // Outputs: Hello, Node.js Developer!
});
await KeywordThe true power comes with the await keyword. You can place await in front of any Promise.
It tells JavaScript to pause the execution of that specific async function until the Promise resolves or rejects. Meanwhile, the rest of your Node.js application (outside the function) keeps running—the event loop is never blocked!
// A dummy promise that takes 2 seconds to resolve
function fetchServerData() {
return new Promise(resolve => {
setTimeout(() => resolve("Data retrieved successfully!"), 2000);
});
}
async function processData() {
console.log("1. Requesting data...");
// The function pauses here until fetchServerData is done
const result = await fetchServerData();
console.log("2.", result);
console.log("3. Processing complete.");
}
processData();
Notice how there are no .then() callbacks! The result of the Promise is directly assigned to the result variable. The code reads top-to-bottom sequentially, making it incredibly intuitive.
Since async/await removes the need for .catch() chains, how do we handle errors if a Promise rejects?
We use the standard JavaScript try...catch block. This is the exact same way you handle errors in synchronous code, which makes async/await highly consistent.
const fs = require('fs').promises;
async function readConfig() {
try {
// Try to read a file
const fileData = await fs.readFile('config.json', 'utf8');
const config = JSON.parse(fileData);
console.log("Database Host:", config.host);
} catch (error) {
// If reading the file fails, or JSON.parse fails, the code jumps here
console.error("Failed to load configuration:");
console.error(error.message);
}
}
readConfig();
By wrapping your await calls inside a try block, you ensure that any rejected Promise is immediately caught in the catch block, preventing application crashes.
Let's look at a side-by-side comparison of fetching a user profile using traditional Promises versus async/await.
Traditional Promise Chain:
function getUserDetails() {
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => console.log("User Posts:", posts))
.catch(err => console.error("Error:", err));
}
With Async/Await:
async function getUserDetails() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
console.log("User Posts:", posts);
} catch (err) {
console.error("Error:", err);
}
}
For developers, the async/await approach drastically reduces cognitive load.
await in a loop sequentially unless one iteration depends on the previous one. If tasks are independent, use Promise.all() to run them concurrently for better performance.try...catch. Unhandled errors in an async function are treated as unhandled Promise rejections.Which JavaScript keyword must be used to declare a function before you can use await inside of it?