Skip to content

Node.js Error Handling — Complete Guide to Errors, Exceptions, and Robust Code

DodaTech Updated 2026-06-28 4 min read

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

Node.js error handling is critical for building reliable applications. Proper error management prevents crashes and provides meaningful feedback when things go wrong.

What You'll Learn

By the end of this tutorial, you'll use try/catch, create custom error classes, handle async errors, manage uncaught exceptions, unhandled rejections, and implement production error patterns.

Why Error Handling Matters

Unhandled errors crash processes. In production, a single uncaught error can bring down your entire server. Proper error handling keeps your application running and helps debug issues effectively.

Real-World Use

An Express.js API middleware catches errors, logs them with context, returns user-friendly error responses, and prevents the server from crashing when a database connection fails.

Error Handling Learning Path

flowchart LR
  A[Worker Threads] --> B[Error Handling]
  B --> C[Debugging]
  C --> D[Testing]
  D --> E[Express.js]
  A --> F{You Are Here}
  style F fill:#f90,color:#fff

Basic Try/Catch

try {
  const data = JSON.parse("invalid json");
} catch (error) {
  console.error("Parse failed:", error.message);
}

Custom Error Classes

class AppError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.name = "AppError";
    this.statusCode = statusCode;
    this.timestamp = new Date().toISOString();
  }
}
class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} not found`, 404);
    this.name = "NotFoundError";
  }
}
try {
  throw new NotFoundError("User");
} catch (err) {
  console.log(`${err.name}: ${err.message} (${err.statusCode})`);
}

Async Error Handling

Async functions return promises. Unhandled promise rejections terminate Node.js.

import fs from "node:fs/promises";

async function readConfig() {
  try {
    const data = await fs.readFile("config.json", "utf8");
    return JSON.parse(data);
  } catch (err) {
    if (err.code === "ENOENT") {
      return { port: 3000 };  // Default config
    }
    throw new AppError(`Config read failed: ${err.message}`);
  }
}

Express Error Middleware

import express from "express";
const app = express();
app.get("/users/:id", async (req, res, next) => {
  try {
    const user = await findUser(req.params.id);
    if (!user) throw new NotFoundError("User");
    res.json(user);
  } catch (err) {
    next(err);  // Forward to error handler
  }
});
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    error: err.message || "Internal Server Error",
    statusCode,
    ...(process.env.NODE_ENV === "development" && { stack: err.stack })
  });
});

Global Error Handlers

process.on("uncaughtException", (err) => {
  console.error("Uncaught exception:", err);
  // Perform cleanup, then exit
  process.exit(1);
});
process.on("unhandledRejection", (reason) => {
  console.error("Unhandled rejection:", reason);
});

Common Mistakes

1. Catching Errors and Doing Nothing

An empty catch block hides errors. Always at least log the error.

2. Using throw in Callbacks Without Try/Catch

Throw inside asynchronous callbacks is not caught by outer try/catch. Use error-first callbacks or promises.

3. Not Differentiating Error Types

Using generic Error for everything makes debugging hard. Create custom error classes with meaningful names.

4. Exposing Internal Error Details in Production

Stack traces and internal error messages leak implementation details. Always sanitize production error responses.

5. Forgetting to Handle Promise Rejections

Unhandled rejections crash Node.js. Always add .catch() or use try/catch with async/await.

Practice Questions

1. What is the difference between an exception and a rejection?

An exception is thrown synchronously (try/catch). A rejection occurs when a promise is rejected (catch handler).

2. How do you create a custom error class in Node.js?

Extend the Error class. Set this.name and add custom properties. The Prototype chain helps identify error types.

3. What happens when an unhandledRejection occurs in Node.js 15+?

Node.js treats unhandled rejections as fatal and exits the Process with a non-zero exit code.

4. Why should Express error middleware have 4 parameters?

Express identifies error-handling middleware by its four parameters (err, req, res, next). Without all four, it's treated as regular middleware.

5. Challenge: Create a safe JSON parser that returns a default value on error instead of crashing.

function safeJSONParse(str, defaultVal = {}) {
  try {
    return JSON.parse(str);
  } catch {
    return defaultVal;
  }
}
console.log(safeJSONParse('{"valid": true}'));  // { valid: true }
console.log(safeJSONParse("invalid"));           // {}

FAQ

What is the difference between throw and return error?

throw stops execution and propagates up the call stack. return error passes it to the caller for handling.

Should I catch uncaughtException in production?

Only for cleanup (close connections, flush logs). Always exit after cleanup because the application state is unreliable.

How do I track error context (user, request, timestamp)?

Use a logger like Winston or Pino with structured metadata. Attach request IDs and user context to each error log.

What is error.cause in Node.js?

Error.cause (ES2022) lets you chain errors: throw new Error('Failed', { cause: originalError }).

How do I test error handling code?

Mock the failing operation (e.g., throw from a mocked function). Assert that the error handler was called with the correct error.

Mini Project: Error Logging Middleware

Build an Express middleware that catches errors, logs them with context, and returns structured responses.

import express from "express";
const app = express();
const errorLogger = (err, req, res, next) => {
  const logEntry = {
    timestamp: new Date().toISOString(),
    method: req.method,
    url: req.url,
    error: err.message,
    stack: err.stack
  };
  console.error(JSON.stringify(logEntry));
  next(err);
};
const errorResponder = (err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    success: false,
    error: statusCode === 500 ? "Internal Server Error" : err.message
  });
};
app.use(errorLogger);
app.use(errorResponder);

What's Next

Node.js Debugging Node.js Testing Express.js Middleware

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro