Skip to content

Node.js Error Handling Deep Dive — Complete Guide to Resilient Systems

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Node.js Error Handling Deep Dive. We cover key concepts, practical examples, and best practices to help you master this topic.

Node.js error handling deep dive covers error classification, EventEmitter error propagation, global uncaughtException handlers, unhandledRejection, and building fault-tolerant Node.js systems.

What You'll Learn

By the end of this tutorial, you'll classify operational vs programmer errors, implement error boundaries, handle EventEmitter errors, configure global exception handlers, and design resilient error recovery.

Why Deep Error Handling Matters

Unhandled errors crash Node.js processes. Production systems need graceful degradation, error isolation, and recovery strategies that prevent total failure across the application.

Real-World Use

An Express API catches operational errors (DB timeout) in middleware and returns 503. Programmer errors (undefined variable) are logged to a monitoring system, and the Process restarts via PM2.

Error Handling Path

flowchart LR
  A[Async Patterns] --> B[Error Handling Deep]
  B --> C[Debugging]
  C --> D[Profiling]
  D --> E[Memory Leaks]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Operational vs Programmer Errors

Classify errors to determine the correct response: operational errors are recoverable, programmer errors indicate bugs.

class OperationalError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.name = "OperationalError";
    this.statusCode = statusCode;
    this.isOperational = true;
  }
}
class ProgrammerError extends Error {
  constructor(message) {
    super(message);
    this.name = "ProgrammerError";
    this.isOperational = false;
  }
}
function divide(a, b) {
  if (b === 0) throw new OperationalError("Division by zero", 400);
  if (typeof a !== "number") throw new ProgrammerError("Invalid argument type");
  return a / b;
}

EventEmitter Error Handling

Always register error listeners on EventEmitters to prevent process crashes.

const { EventEmitter } = require("node:events");
const emitter = new EventEmitter();
emitter.on("error", (err) => {
  console.error("Event error caught:", err.message);
});
emitter.emit("error", new OperationalError("Database connection failed", 503));
const safeEmitter = new EventEmitter();
if (safeEmitter.listenerCount("error") === 0) {
  safeEmitter.on("error", (err) => {
    console.error("Default error handler:", err.message);
  });
}

uncaughtException and unhandledRejection

Handle globally uncaught errors and promise rejections as last resort.

process.on("uncaughtException", (err) => {
  console.error("UNCAUGHT EXCEPTION:", err);
  process.exit(1);
});
process.on("unhandledRejection", (reason) => {
  console.error("UNHANDLED REJECTION:", reason);
});
process.on("warning", (warning) => {
  if (warning.name === "UnhandledPromiseRejectionWarning") {
    console.error("Promise rejection detected:", warning.message);
  }
});

Express Error Middleware

Implement centralized error handling in Express with proper status codes.

function errorMiddleware(err, req, res, next) {
  if (err.isOperational) {
    return res.status(err.statusCode || 500).json({
      error: err.message,
      code: err.code || "OPERATIONAL_ERROR",
    });
  }
  console.error("PROGRAMMER ERROR:", err);
  res.status(500).json({
    error: "Internal server error",
    code: "INTERNAL_ERROR",
  });
}
app.use(errorMiddleware);

Async Handler Wrapper

Wrap async route handlers to catch promise rejections automatically.

function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}
app.get("/users/:id", asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) throw new OperationalError("User not found", 404);
  res.json(user);
}));

Common Mistakes

1. Exiting on unhandledRejection

unhandledRejection should not crash the process. Log and continue. uncaughtException should exit.

2. Overusing Try-Catch Around Every Call

Wrap logical blocks, not every line. Too many try-catch blocks hide real errors.

3. Not Differentiating Error Types

Treating all errors the same prevents appropriate responses. Use error subclasses.

4. Ignoring Error Events on Streams

Streams emit error events. Always attach error listeners to stream.pipeline.

5. Swallowing Errors in Callbacks

Callbacks that receive errors must handle or forward them. Silent failures are the hardest to debug.

Practice Questions

1. What is the difference between operational and programmer errors?

Operational errors are runtime issues (DB down, timeout). Programmer errors are bugs (undefined variable, wrong type).

2. Should you exit the process on unhandledRejection?

No. Promise rejections are recoverable. Log and continue. Only exit on uncaughtException.

3. What does asyncHandler do in Express?

Wraps async route handlers so promise rejections are forwarded to error middleware via next().

4. Why should you always listen for error events on EventEmitters?

Unhandled error events throw and crash the process. Always register error listeners.

5. Challenge: Create an error handling system with custom error classes and logging.

class AppError extends Error {
  constructor(message, statusCode, code) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = true;
  }
}
class NotFoundError extends AppError {
  constructor(resource = "Resource") {
    super(`${resource} not found`, 404, "NOT_FOUND");
  }
}
function errorHandler(err, req, res, next) {
  if (err.isOperational) {
    return res.status(err.statusCode).json({ code: err.code, message: err.message });
  }
  console.error("Unexpected error:", err);
  res.status(500).json({ code: "INTERNAL_ERROR", message: "Something went wrong" });
}

FAQ

What is the difference between uncaughtException and unhandledRejection?

uncaughtException catches synchronous errors. unhandledRejection catches promise rejections without a .catch() handler.

Should I use the Node.js domains module?

No. Domains are deprecated. Use async/await with try-catch or error middleware patterns instead.

How do I log errors in production?

Use structured logging (JSON format) with correlation IDs. Send to centralized logging systems like ELK or Datadog.

What is the best practice for process exit on error?

Exit only on uncaughtException after logging. Let the process manager (PM2, Docker) restart. Never exit on promise rejections.

Can I recover from an uncaughtException?

The process is in an unknown state. Log, alert, and exit. Do not attempt to continue running.

Mini Project: Error Monitoring Middleware

Build Express middleware that captures and reports errors to a monitoring system.

class ErrorMonitor {
  constructor() {
    this.errors = [];
  }
  middleware() {
    return (err, req, res, next) => {
      this.errors.push({
        message: err.message,
        stack: err.stack,
        path: req.path,
        method: req.method,
        timestamp: new Date().toISOString(),
        isOperational: err.isOperational || false,
      });
      if (this.errors.length > 100) this.errors.shift();
      next(err);
    };
  }
  getReport() {
    return {
      total: this.errors.length,
      operational: this.errors.filter((e) => e.isOperational).length,
      programmer: this.errors.filter((e) => !e.isOperational).length,
      recent: this.errors.slice(-10),
    };
  }
}

What's Next

Node.js Debugging Node.js Profiling Node.js PM2

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro