Robust Error Handling in Node.js

Resilient services are defined by how they handle failure

Anything that can fail, will. The difference between a fragile script and a production service is disciplined error handling. This guide covers the patterns that keep Node apps stable.

Two Kinds of Errors

  • Operational errors — expected runtime problems (network down, bad input). Handle and recover.
  • Programmer errors — bugs (undefined is not a function). Fix the code; often best to crash and restart.

Conflating the two leads to apps that swallow real bugs while crashing on routine hiccups.

Handling Async Errors

async function getUser(id) {
  try {
    const user = await db.find(id);
    if (!user) throw new NotFoundError('user');
    return user;
  } catch (err) {
    logger.error({ err, id });
    throw err; // let the caller/middleware decide the response
  }
}

Centralised error middleware keeps responses consistent

Centralised Express Error Middleware

Define one error handler last so every route funnels failures through it:

Concern Where it lives
Logging Error middleware
Status mapping Error middleware (404, 400, 500)
Safe client message Error middleware (never leak stack traces)

Last-Resort Safety Nets

  1. Listen for unhandledRejection and uncaughtException — log, then exit cleanly.
  2. Run under a process manager (PM2, systemd, container) that restarts on crash.
  3. Fail fast on programmer errors; never keep a process in an unknown state.

Crash on bugs, recover from expected failures. Knowing which is which is the whole game.

What to Learn Next

  • Custom error classes and typed errors
  • Structured logging with correlation IDs
  • Graceful shutdown on SIGTERM

Arivanandhan Chitheshwaran