Async Patterns: Callbacks → Promises → async/await
14 min read
Async Patterns in Node.js
Node.js async code has evolved through three eras. Understanding all three helps you read legacy codebases and write clean modern code.
1. Callbacks (the original pattern)
Early Node.js APIs use the error-first callback convention: the first argument is always an error (or null), the second is the result.
js
const fs = require("fs");
fs.readFile("data.txt", "utf8", function(err, data) {
if (err) {
return console.error("Error:", err.message);
}
fs.writeFile("output.txt", data.toUpperCase(), function(err) {
if (err) {
return console.error("Write error:", err.message);
}
console.log("Done!");
});
});2. Promises
Promises chain .then() and .catch() to avoid deep nesting. You can also convert callback-based functions using util.promisify.
js
const { promisify } = require("util");
const fs = require("fs");
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
readFile("data.txt", "utf8")
.then(function(data) {
return writeFile("output.txt", data.toUpperCase());
})
.then(function() {
console.log("Done!");
})
.catch(function(err) {
console.error("Error:", err.message);
});3. async/await (recommended)
async/await is syntactic sugar over Promises. It makes async code read like synchronous code, improving readability significantly. Mark a function as async and await any Promise inside it.
js
const fs = require("fs/promises");
async function processData() {
try {
const data = await fs.readFile("data.txt", "utf8");
await fs.writeFile("output.txt", data.toUpperCase());
console.log("Done!");
} catch (err) {
console.error("Error:", err.message);
}
}
processData();Use Promise.all([p1, p2, p3]) to run multiple async operations in parallel instead of awaiting them sequentially. This can dramatically reduce total wait time.
Ready to test yourself?
Practice Node.js— quiz & coding exercises