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

javascript
// 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

javascript
// 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

javascript
// Bad: sync heavy work blocks all clients
// const data = fs.readFileSync("./huge.log");
 
// Better: async I/O or worker threads for CPU-heavy jobs

Long CPU tasks (image processing, big JSON parse) stall every request—offload to workers or separate services.

Timeouts for External Calls

javascript
// 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

javascript
// 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

javascript
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

javascript
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.

What comes next?

Environment variables.