Async Patterns & Errors
Node is built for asynchronous work. Knowing how to write and handle async code — and how to fail gracefully — is essential.
Callbacks (the original)
Early Node used error-first callbacks:
import { readFile } from "node:fs";
readFile("notes.txt", "utf8", (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
The convention: the first argument is an error, the second the result. This works but nests badly — “callback hell”.
Promises
A Promise represents a future value. It is either pending, fulfilled, or rejected:
import { readFile } from "node:fs/promises";
readFile("notes.txt", "utf8")
.then((data) => console.log(data))
.catch((err) => console.error(err));
async / await
async/await lets you write async code that reads like synchronous code:
async function load() {
try {
const data = await readFile("notes.txt", "utf8");
console.log(data);
} catch (err) {
console.error(err);
}
}
An async function always returns a Promise. await pauses that function only — the rest of the program keeps running.
Running in Parallel
Do not await independent tasks one by one:
// slow — sequential
const a = await getUser(1);
const b = await getUser(2);
// fast — parallel
const [a, b] = await Promise.all([getUser(1), getUser(2)]);
Promise.all rejects as soon as any promise rejects. Promise.allSettled waits for all and reports each result.
Central Error Handling
Do not swallow errors. Either handle them where you can recover, or let them bubble:
async function main() {
await startServer();
}
main().catch((err) => {
console.error("Fatal:", err);
process.exit(1);
});
Async Iteration
Streams and async generators work with for await...of:
for await (const chunk of stream) {
process.stdout.write(chunk);
}
Best Practices
- Prefer async/await — Clearer than raw promises or callbacks.
- Await in parallel when independent —
Promise.allcuts latency. - Never leave a promise unhandled — It can crash the process.
- Handle errors at the boundary — Top-level
.catchas a last resort.
Common Mistakes
- Forgetting
await— You get a Promise, not the value. awaitinside a loop — Runs sequentially; batch instead.- Swallowing errors — Empty
catchblocks hide bugs. - Assuming order — Async callbacks do not run in the order you wrote them.