Node's Original Async Style — Still Everywhere
Callbacks
Pass a function to be called when work finishes. Node's classic pattern — superseded by promises but still around.
What you'll learn
- Recognize the (err, result) callback convention
- Know why callbacks fell out of favor
- Convert callbacks to promises with promisify
Before promises, Node used callbacks for everything async. You’d pass a function that the runtime calls when the work finishes.
The Pattern
import { readFile } from "node:fs";
readFile("data.txt", "utf8", (err, content) => {
if (err) {
console.error("read failed", err);
return;
}
console.log(content);
}); Node’s convention: callback’s first argument is always the error
(or null). Remaining args are the result. Same shape across the
whole standard library.
The Problem — Nesting
Stack a few of them and you get the famous “callback pyramid”:
readFile("a.txt", "utf8", (err, a) => {
if (err) return done(err);
readFile("b.txt", "utf8", (err, b) => {
if (err) return done(err);
writeFile("c.txt", a + b, (err) => {
if (err) return done(err);
done(null, "all done");
});
});
}); Hard to read, hard to add error handling, hard to compose.
Convert to Promises
util.promisify wraps a Node-style callback function as a promise:
import { readFile, writeFile } from "node:fs";
import { promisify } from "node:util";
const read = promisify(readFile);
const write = promisify(writeFile);
const a = await read("a.txt", "utf8");
const b = await read("b.txt", "utf8");
await write("c.txt", a + b); For the standard library, just import from /promises —
node:fs/promises, node:dns/promises, node:timers/promises.
When You’ll Still See Callbacks
- Old library APIs (
request,mongojs, etc.) - Stream events (
stream.on("data", ...)) - EventEmitter listeners
- Some Node APIs (e.g.
crypto.scrypt— has both)
Knowing the pattern means you can read older code and convert it.
Promises →