The Default Way to Write Async Node Code
async/await
async functions return promises. await pauses execution until a promise resolves. The default style for Node.
What you'll learn
- Write async functions
- Await sequentially and in parallel
- Handle errors with try/catch
async/await is sugar over promises. Same machinery — far better
syntax.
A Basic Function
import { readFile } from "node:fs/promises";
async function loadConfig() {
const text = await readFile("config.json", "utf8");
return JSON.parse(text);
}
const config = await loadConfig(); asyncon a function → it always returns a promiseawaitpauses until the promise settlesreturn valueresolves the promise;throw errrejects it
Sequential
async function run() {
const a = await readFile("a.txt", "utf8"); // wait
const b = await readFile("b.txt", "utf8"); // then wait
return a + b;
} Total time: file A + file B (each step waits for the previous).
Parallel
async function run() {
const [a, b] = await Promise.all([
readFile("a.txt", "utf8"),
readFile("b.txt", "utf8"),
]);
return a + b;
} Total time: max(A, B). Use parallel whenever steps don’t depend on each other.
Errors
async function safeRead(p) {
try {
return await readFile(p, "utf8");
} catch (err) {
if (err.code === "ENOENT") return null;
throw err;
}
} try/catch works the same as sync code. Reject = throw.
Top-Level Await
In ESM modules, await works at the top level — no wrapper needed:
// app.mjs
import { readFile } from "node:fs/promises";
const config = JSON.parse(await readFile("config.json", "utf8"));
console.log(config); For CJS scripts: wrap in an async IIFE — (async () => { ... })().
A Forever Gotcha
forEach doesn’t await:
async function run() {
files.forEach(async (f) => {
await process(f); // each is fire-and-forget!
});
console.log("done"); // runs *before* the processing finishes
} Use a regular for...of:
for (const f of files) {
await process(f);
} Or Promise.all(files.map(process)) for parallel.