Asynchronous Node
Nearly everything Node does that touches a disk or a network is asynchronous by default. You've already seen the callback style — this lesson covers the two patterns built on top of it that make async code far easier to read.
The callback style, revisited
The classic Node pattern hands a function an "error-first" callback — the first argument is either an error or null:
const fs = require("fs");
fs.readFile("data.txt", "utf8", (err, data) => {
if (err) {
console.error("Failed:", err.message);
return;
}
console.log("Got:", data);
});
This works fine for one operation. Chain three or four dependent reads together and it turns into deeply nested "callback hell" — which is exactly what Promises were built to fix.
Promises with fs.promises
Node's built-in modules ship promise-based versions alongside the callback ones:
const fs = require("fs/promises");
fs.readFile("data.txt", "utf8")
.then((data) => console.log("Got:", data))
.catch((err) => console.error("Failed:", err.message));
async/await
async/await is syntax sugar over Promises, letting asynchronous code read top-to-bottom like synchronous code:
const fs = require("fs/promises");
async function loadData() {
try {
const data = await fs.readFile("data.txt", "utf8");
console.log("Got:", data);
} catch (err) {
console.error("Failed:", err.message);
}
}
loadData();
Got: some file contents
All three versions do the same thing — read a file without blocking the rest of the program — but async/await is what most modern Node code reaches for, because a try/catch block is a lot easier to follow than a chain of .then() calls once more than one async step is involved.
.catch() and no surrounding try/catch on an await — crashes the entire process by default, rather than failing silently. Always handle the error case on any promise you don't immediately await inside a try block.