Node.js Async and Await
Async/await is the modern, most readable way to write asynchronous code in Node.js. It is built on top of Promises and allows asynchronous code to be written in a style that looks and reads almost like regular synchronous code — without the complexity of chaining .then() calls.
The async keyword is used to declare a function as asynchronous. Inside an async function, the await keyword pauses execution until a Promise resolves, and then returns the result — all without blocking the rest of the program.
The async Keyword
When a function is declared with the async keyword, it automatically returns a Promise — even if the function just returns a plain value:
async function greet() {
return "Hello from an async function!";
}
greet().then(function(message) {
console.log(message);
});
// Output: Hello from an async function!
The function returns a string, but because it is async, it is automatically wrapped in a resolved Promise.
The await Keyword
await can only be used inside an async function. It pauses the async function at that point and waits for the Promise to resolve, then returns the resolved value:
function fetchData() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve("Data loaded successfully!");
}, 1000);
});
}
async function main() {
console.log("Fetching data...");
const result = await fetchData();
console.log(result);
console.log("Done.");
}
main();
Output:
Fetching data...
Data loaded successfully!
Done.
The code inside main() looks synchronous but is actually non-blocking. While waiting for fetchData(), the Node.js event loop is free to handle other tasks.
Error Handling with try/catch
With async/await, errors are handled using standard try/catch blocks — the same way synchronous errors are caught:
function getUserById(id) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
if (id === 1) {
resolve({ id: 1, name: "Alice" });
} else {
reject(new Error("User not found"));
}
}, 800);
});
}
async function showUser(id) {
try {
const user = await getUserById(id);
console.log("User:", user.name);
} catch (err) {
console.log("Error:", err.message);
}
}
showUser(1); // User: Alice
showUser(99); // Error: User not found
Sequential Async Operations
Async/await shines when multiple asynchronous operations need to happen one after another:
function step(name, delay) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(name + " completed");
}, delay);
});
}
async function runSteps() {
console.log("Starting workflow...");
const result1 = await step("Login", 500);
console.log(result1);
const result2 = await step("Load Dashboard", 700);
console.log(result2);
const result3 = await step("Fetch Reports", 400);
console.log(result3);
console.log("All steps completed.");
}
runSteps();
Output:
Starting workflow...
Login completed
Load Dashboard completed
Fetch Reports completed
All steps completed.
Each step waits for the previous one to finish before starting — easy to read and maintain.
Parallel Execution with Promise.all and await
When multiple tasks do not depend on each other, they can run in parallel for better performance:
async function fetchParallel() {
const task1 = step("Task A", 1000);
const task2 = step("Task B", 800);
const task3 = step("Task C", 600);
// Start all tasks and wait for all to finish
const [r1, r2, r3] = await Promise.all([task1, task2, task3]);
console.log(r1);
console.log(r2);
console.log(r3);
}
fetchParallel();
Output (all finish in ~1 second total instead of ~2.4 seconds sequentially):
Task A completed
Task B completed
Task C completed
Using Async/Await with the fs Module
The promise-based version of fs works directly with async/await:
const fs = require('fs').promises;
async function readAndWrite() {
try {
// Read a file
const data = await fs.readFile('input.txt', 'utf8');
console.log("Read:", data);
// Write to another file
await fs.writeFile('output.txt', "Processed: " + data);
console.log("Written to output.txt");
} catch (err) {
console.log("File error:", err.message);
}
}
readAndWrite();
Async Functions in Loops
When using await inside a loop, be aware of whether operations should run sequentially or in parallel:
Sequential Loop (one at a time)
async function processItems() {
const items = ["Apple", "Banana", "Cherry"];
for (const item of items) {
await step(item, 300); // Each waits for the previous
console.log("Processed:", item);
}
}
processItems();
Parallel Loop (all at once)
async function processAllParallel() {
const items = ["Apple", "Banana", "Cherry"];
const results = await Promise.all(
items.map(item => step(item, 300))
);
results.forEach(r => console.log(r));
}
processAllParallel();
Use a regular for...of loop for sequential tasks and Promise.all with .map() for parallel tasks.
Async/Await vs Callbacks vs Promises
| Feature | Callbacks | Promises | Async/Await |
|---|---|---|---|
| Readability | Low (callback hell) | Medium (.then chains) | High (looks synchronous) |
| Error Handling | Manual checks | .catch() at end | try/catch block |
| Chaining | Nested functions | .then() chains | Straightforward sequential code |
| Debugging | Harder | Moderate | Easiest |
Key Points
asyncmarks a function as asynchronous and ensures it always returns a Promise.awaitpauses anasyncfunction until the awaited Promise resolves and returns its value.- Error handling in async/await uses
try/catch, making it consistent with synchronous error handling. - Sequential async operations are written naturally one after another using
await. - For parallel tasks,
Promise.all()combined withawaitgives the best performance. - Async/await is built on top of Promises — understanding Promises first makes async/await easier to grasp.
- Async/await is the recommended, modern approach for writing asynchronous code in Node.js.
