Async and Reliability in Node.js
Introduction
Node servers handle many concurrent connections on one thread by using non-blocking I/O. Poor async patterns—unhandled rejections, blocking CPU work, missing error handlers—cause crashes and hung processes. This chapter covers reliable async style, process signals, and operational basics.
Prerequisites
Prefer async/await in Servers
// route-handler.mjs
import fs from "node:fs/promises";
async function loadUsers() {
const text = await fs.readFile("./users.json", "utf8");
return JSON.parse(text);
}
loadUsers()
.then((users) => console.log(users.length))
.catch((err) => console.error("loadUsers failed", err));Wrap top-level server startup in try/catch or .catch.
Unhandled Rejection and Exceptions
// process.mjs — register early in entry file
process.on("unhandledRejection", (reason) => {
console.error("unhandledRejection", reason);
});
process.on("uncaughtException", (err) => {
console.error("uncaughtException", err);
process.exit(1);
});Fix the root cause—do not rely on these handlers in production without logging and exit policy.
Do Not Block the Event Loop
// Bad: sync heavy work blocks all clients
// const data = fs.readFileSync("./huge.log");
// Better: async I/O or worker threads for CPU-heavy jobsLong CPU tasks (image processing, big JSON parse) stall every request—offload to workers or separate services.
Timeouts for External Calls
// fetch with AbortController
async function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
const res = await fetch(url, { signal: controller.signal });
return res;
} finally {
clearTimeout(timer);
}
}Graceful Shutdown
// graceful.mjs
import http from "node:http";
const server = http.createServer((req, res) => {
res.end("ok\n");
});
server.listen(3000);
function shutdown(signal) {
console.log(signal, "closing server");
server.close(() => {
console.log("closed");
process.exit(0);
});
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);Finish in-flight requests before exit in production orchestrators (Kubernetes, PM2).
Structured Error Responses
async function handler(req, res) {
try {
const data = await riskyWork();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
} catch (err) {
console.error(err);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "internal_error" }));
}
}Do not leak stack traces to public clients.
Mini Example: Retry with Backoff
async function retry(fn, attempts = 3) {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (err) {
lastError = err;
const delay = 100 * 2 ** i;
await new Promise((r) => setTimeout(r, delay));
}
}
throw lastError;
}
await retry(async () => {
const res = await fetch("https://example.com");
if (!res.ok) throw new Error("bad status");
return res.status;
}).then(console.log);FAQ
Cluster mode?
node:cluster forks workers for multi-core CPUs—frameworks and PM2 often handle this.
Logging?
Use a logger (pino, winston) with levels—not only console.log in production.
Testing async code?
Test runners support async tests—see plan section on quality; use vitest or node:test.