Nodejs

Node.js Error Handling Strategies for Production

A practical guide to Node.js error handling covering operational vs programmer errors, async error patterns, Express error middleware, graceful shutdown, and error monitoring.

Node.js Error Handling Strategies for Production

Errors in production are inevitable. Network connections drop, databases timeout, users submit malformed data, and edge cases your tests did not cover surface under real traffic. The difference between a reliable application and a fragile one is not whether errors occur — it is how the application handles them.

Node.js error handling has specific patterns because of its asynchronous, event-driven architecture. Callbacks, Promises, event emitters, and streams each have different error propagation mechanisms. Missing an error in any of these can crash the process. This guide covers error handling patterns that keep Node.js applications running reliably in production.

Two Types of Errors

Understanding the distinction between operational and programmer errors determines how you handle each.

Operational Errors

Expected failures in normal operation:

  • Database connection timeouts
  • Network request failures
  • Invalid user input
  • File not found
  • Rate limit exceeded
  • Disk full

These are runtime problems. The code is correct, but something external failed. Handle them gracefully — retry, return an error response, log, and continue.

Programmer Errors

Bugs in your code:

  • Reading a property of undefined
  • Calling a function with wrong argument types
  • Off-by-one errors
  • Logic errors
  • Missing await on a Promise

These indicate the code is wrong. The safest response is to crash and restart the process. A corrupted state is worse than a brief restart.

Synchronous Error Handling

Try-Catch

function parseConfig(jsonString) {
  try {
    var config = JSON.parse(jsonString);
    if (!config.port) {
      throw new Error("Missing required field: port");
    }
    return config;
  } catch (err) {
    console.error("Config parse error:", err.message);
    return null;
  }
}

Custom Error Classes

// errors.js
function AppError(message, statusCode, code) {
  Error.call(this);
  Error.captureStackTrace(this, AppError);
  this.name = "AppError";
  this.message = message;
  this.statusCode = statusCode || 500;
  this.code = code || "INTERNAL_ERROR";
  this.isOperational = true;
}
AppError.prototype = Object.create(Error.prototype);
AppError.prototype.constructor = AppError;

function NotFoundError(resource) {
  AppError.call(this, resource + " not found", 404, "NOT_FOUND");
  this.name = "NotFoundError";
}
NotFoundError.prototype = Object.create(AppError.prototype);
NotFoundError.prototype.constructor = NotFoundError;

function ValidationError(message, fields) {
  AppError.call(this, message, 400, "VALIDATION_ERROR");
  this.name = "ValidationError";
  this.fields = fields || {};
}
ValidationError.prototype = Object.create(AppError.prototype);
ValidationError.prototype.constructor = ValidationError;

function UnauthorizedError(message) {
  AppError.call(this, message || "Authentication required", 401, "UNAUTHORIZED");
  this.name = "UnauthorizedError";
}
UnauthorizedError.prototype = Object.create(AppError.prototype);
UnauthorizedError.prototype.constructor = UnauthorizedError;

module.exports = {
  AppError: AppError,
  NotFoundError: NotFoundError,
  ValidationError: ValidationError,
  UnauthorizedError: UnauthorizedError
};
var errors = require("./errors");

function getUser(id) {
  return db.query("SELECT * FROM users WHERE id = $1", [id])
    .then(function(result) {
      if (result.rows.length === 0) {
        throw new errors.NotFoundError("User");
      }
      return result.rows[0];
    });
}

function createUser(data) {
  var validationErrors = {};

  if (!data.email) validationErrors.email = "Email is required";
  if (!data.name) validationErrors.name = "Name is required";

  if (Object.keys(validationErrors).length > 0) {
    throw new errors.ValidationError("Invalid input", validationErrors);
  }

  return db.query(
    "INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *",
    [data.email, data.name]
  );
}

Asynchronous Error Handling

Callback Errors

The Node.js callback convention passes errors as the first argument:

var fs = require("fs");

fs.readFile("/path/to/config.json", "utf8", function(err, data) {
  if (err) {
    if (err.code === "ENOENT") {
      console.log("Config file not found, using defaults");
      return useDefaults();
    }
    console.error("Failed to read config:", err.message);
    return;
  }

  var config = JSON.parse(data);
  startApp(config);
});

Always check the error argument. Ignoring it leads to silent failures.

Promise Errors

Every Promise chain must have a .catch() handler:

// BAD — unhandled rejection if query fails
function getUsers() {
  return db.query("SELECT * FROM users")
    .then(function(result) {
      return result.rows;
    });
}

// GOOD — error is caught and handled
function getUsers() {
  return db.query("SELECT * FROM users")
    .then(function(result) {
      return result.rows;
    })
    .catch(function(err) {
      console.error("Failed to fetch users:", err.message);
      throw err; // Re-throw to let caller handle it
    });
}

Promise Chain Error Handling

function processOrder(orderId) {
  return getOrder(orderId)
    .then(function(order) {
      return validateInventory(order);
    })
    .then(function(order) {
      return chargePayment(order);
    })
    .then(function(order) {
      return sendConfirmation(order);
    })
    .catch(function(err) {
      // Catch errors from any step in the chain
      if (err instanceof errors.NotFoundError) {
        console.log("Order not found:", orderId);
      } else if (err.code === "INSUFFICIENT_INVENTORY") {
        console.log("Inventory unavailable for order:", orderId);
      } else if (err.code === "PAYMENT_FAILED") {
        console.log("Payment failed for order:", orderId);
      } else {
        console.error("Unexpected error processing order:", err);
      }
      throw err;
    });
}

Retry Pattern for Transient Errors

function withRetry(fn, options) {
  var maxRetries = (options && options.maxRetries) || 3;
  var delay = (options && options.delay) || 1000;
  var backoff = (options && options.backoff) || 2;

  function attempt(retriesLeft) {
    return fn().catch(function(err) {
      if (retriesLeft <= 0) {
        throw err;
      }

      // Only retry operational errors
      if (err.code === "ECONNREFUSED" || err.code === "ETIMEDOUT" || err.code === "ECONNRESET") {
        var waitTime = delay * Math.pow(backoff, maxRetries - retriesLeft);
        console.log("Retrying in " + waitTime + "ms (" + retriesLeft + " retries left)");

        return new Promise(function(resolve) {
          setTimeout(resolve, waitTime);
        }).then(function() {
          return attempt(retriesLeft - 1);
        });
      }

      // Non-retryable errors propagate immediately
      throw err;
    });
  }

  return attempt(maxRetries);
}

// Usage
function fetchWithRetry(url) {
  return withRetry(function() {
    return httpClient.get(url);
  }, { maxRetries: 3, delay: 1000, backoff: 2 });
}

Express Error Handling

Route-Level Error Handling

// Routes that return Promises — Express 4 does not catch Promise rejections
router.get("/users/:id", function(req, res, next) {
  getUser(req.params.id)
    .then(function(user) {
      res.json(user);
    })
    .catch(next); // Pass errors to Express error handler
});

// Wrapper to catch async errors automatically
function asyncHandler(fn) {
  return function(req, res, next) {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Cleaner route definitions
router.get("/users/:id", asyncHandler(function(req, res) {
  return getUser(req.params.id).then(function(user) {
    res.json(user);
  });
}));

router.post("/users", asyncHandler(function(req, res) {
  return createUser(req.body).then(function(result) {
    res.status(201).json(result.rows[0]);
  });
}));

Express Error Middleware

Error-handling middleware has four parameters — err, req, res, next:

// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
  // Log the error
  if (err.isOperational) {
    console.warn("Operational error:", {
      message: err.message,
      code: err.code,
      path: req.path,
      method: req.method
    });
  } else {
    console.error("Programmer error:", {
      message: err.message,
      stack: err.stack,
      path: req.path,
      method: req.method
    });
  }

  // Determine status code
  var statusCode = err.statusCode || 500;

  // Build response
  var response = {
    error: {
      message: statusCode === 500 ? "Internal server error" : err.message,
      code: err.code || "INTERNAL_ERROR"
    }
  };

  // Include validation details for 400 errors
  if (err.fields) {
    response.error.fields = err.fields;
  }

  // Include stack trace in development only
  if (process.env.NODE_ENV !== "production" && err.stack) {
    response.error.stack = err.stack;
  }

  res.status(statusCode).json(response);
}

module.exports = errorHandler;

404 Handler

// middleware/notFound.js
function notFound(req, res) {
  res.status(404).json({
    error: {
      message: "Route not found: " + req.method + " " + req.path,
      code: "NOT_FOUND"
    }
  });
}

module.exports = notFound;

Wiring It Together

// app.js
var express = require("express");
var app = express();
var errorHandler = require("./middleware/errorHandler");
var notFound = require("./middleware/notFound");

// Body parsing
app.use(express.json());

// Routes
app.use("/api/users", require("./routes/users"));
app.use("/api/articles", require("./routes/articles"));

// 404 handler — must be after all routes
app.use(notFound);

// Error handler — must be last
app.use(errorHandler);

module.exports = app;

Process-Level Error Handling

Uncaught Exceptions

An uncaught exception means a programmer error escaped all error handlers. The process state may be corrupted:

// server.js
process.on("uncaughtException", function(err) {
  console.error("UNCAUGHT EXCEPTION:", {
    message: err.message,
    stack: err.stack,
    timestamp: new Date().toISOString()
  });

  // Attempt graceful shutdown
  shutdownGracefully(1);
});

Unhandled Promise Rejections

A Promise rejected without a .catch() handler:

process.on("unhandledRejection", function(reason, promise) {
  console.error("UNHANDLED REJECTION:", {
    reason: reason instanceof Error ? reason.message : String(reason),
    stack: reason instanceof Error ? reason.stack : undefined,
    timestamp: new Date().toISOString()
  });

  // In Node.js 15+, unhandled rejections crash the process by default.
  // In earlier versions, log and optionally exit.
  shutdownGracefully(1);
});

Graceful Shutdown

When the process needs to exit, finish handling in-progress requests before terminating:

// server.js
var http = require("http");
var app = require("./app");
var db = require("./db");

var server = http.createServer(app);
var port = process.env.PORT || 3000;
var isShuttingDown = false;

server.listen(port, function() {
  console.log("Server started on port " + port);
});

function shutdownGracefully(exitCode) {
  if (isShuttingDown) return;
  isShuttingDown = true;

  console.log("Starting graceful shutdown...");

  // Stop accepting new connections
  server.close(function() {
    console.log("HTTP server closed");

    // Close database connections
    db.end()
      .then(function() {
        console.log("Database connections closed");
        process.exit(exitCode || 0);
      })
      .catch(function(err) {
        console.error("Error closing database:", err);
        process.exit(exitCode || 1);
      });
  });

  // Force close after 30 seconds
  setTimeout(function() {
    console.error("Forced shutdown after timeout");
    process.exit(exitCode || 1);
  }, 30000);
}

// Handle shutdown signals
process.on("SIGTERM", function() {
  console.log("SIGTERM received");
  shutdownGracefully(0);
});

process.on("SIGINT", function() {
  console.log("SIGINT received");
  shutdownGracefully(0);
});

process.on("uncaughtException", function(err) {
  console.error("Uncaught exception:", err);
  shutdownGracefully(1);
});

process.on("unhandledRejection", function(reason) {
  console.error("Unhandled rejection:", reason);
  shutdownGracefully(1);
});

Event Emitter Errors

Event emitters that emit an error event without a listener will crash the process:

var EventEmitter = require("events");

var emitter = new EventEmitter();

// BAD — crashes the process
emitter.emit("error", new Error("something failed"));

// GOOD — handle error events
emitter.on("error", function(err) {
  console.error("Emitter error:", err.message);
});

// For streams
var fs = require("fs");
var readStream = fs.createReadStream("/path/to/file");

readStream.on("error", function(err) {
  if (err.code === "ENOENT") {
    console.log("File not found");
  } else {
    console.error("Stream error:", err);
  }
});

readStream.on("data", function(chunk) {
  // Process data
});

Error Monitoring

Structured Error Logging

// logger.js
function logError(err, context) {
  var entry = {
    level: "error",
    timestamp: new Date().toISOString(),
    message: err.message,
    code: err.code,
    statusCode: err.statusCode,
    isOperational: err.isOperational || false,
    stack: err.stack
  };

  if (context) {
    entry.context = context;
  }

  console.error(JSON.stringify(entry));
}

module.exports = { logError: logError };

Error Rate Tracking

// monitor/errors.js
var errorCounts = {
  total: 0,
  operational: 0,
  programmer: 0,
  byCode: {}
};

var windowStart = Date.now();
var WINDOW_MS = 60000; // 1 minute window

function trackError(err) {
  var now = Date.now();

  // Reset window
  if (now - windowStart > WINDOW_MS) {
    if (errorCounts.total > 0) {
      console.log("Error stats (last minute):", JSON.stringify(errorCounts));
    }
    errorCounts = { total: 0, operational: 0, programmer: 0, byCode: {} };
    windowStart = now;
  }

  errorCounts.total++;

  if (err.isOperational) {
    errorCounts.operational++;
  } else {
    errorCounts.programmer++;
  }

  var code = err.code || "UNKNOWN";
  errorCounts.byCode[code] = (errorCounts.byCode[code] || 0) + 1;
}

function getErrorStats() {
  return Object.assign({}, errorCounts, {
    windowStarted: new Date(windowStart).toISOString()
  });
}

module.exports = { trackError: trackError, getErrorStats: getErrorStats };

Health Check with Error Awareness

var errorMonitor = require("./monitor/errors");

app.get("/health", function(req, res) {
  var stats = errorMonitor.getErrorStats();
  var healthy = stats.programmer === 0 && stats.total < 100;

  res.status(healthy ? 200 : 503).json({
    status: healthy ? "healthy" : "degraded",
    uptime: process.uptime(),
    errors: stats
  });
});

Timeouts

Database Query Timeouts

function queryWithTimeout(text, params, timeoutMs) {
  var timeout = timeoutMs || 5000;

  return new Promise(function(resolve, reject) {
    var timer = setTimeout(function() {
      reject(new errors.AppError("Query timeout after " + timeout + "ms", 503, "QUERY_TIMEOUT"));
    }, timeout);

    db.query(text, params)
      .then(function(result) {
        clearTimeout(timer);
        resolve(result);
      })
      .catch(function(err) {
        clearTimeout(timer);
        reject(err);
      });
  });
}

HTTP Request Timeouts

var http = require("http");

function fetchWithTimeout(url, timeoutMs) {
  return new Promise(function(resolve, reject) {
    var timer = setTimeout(function() {
      req.destroy();
      reject(new errors.AppError("Request timeout", 503, "REQUEST_TIMEOUT"));
    }, timeoutMs || 10000);

    var req = http.get(url, function(res) {
      var data = "";
      res.on("data", function(chunk) { data += chunk; });
      res.on("end", function() {
        clearTimeout(timer);
        resolve(data);
      });
    });

    req.on("error", function(err) {
      clearTimeout(timer);
      reject(err);
    });
  });
}

Circuit Breaker Pattern

Prevent cascading failures by stopping requests to a failing service:

// circuitBreaker.js
function CircuitBreaker(options) {
  this.failureThreshold = (options && options.failureThreshold) || 5;
  this.resetTimeout = (options && options.resetTimeout) || 30000;
  this.state = "CLOSED"; // CLOSED = normal, OPEN = blocking, HALF_OPEN = testing
  this.failureCount = 0;
  this.lastFailure = null;
  this.successCount = 0;
}

CircuitBreaker.prototype.execute = function(fn) {
  var self = this;

  if (this.state === "OPEN") {
    if (Date.now() - this.lastFailure > this.resetTimeout) {
      this.state = "HALF_OPEN";
      this.successCount = 0;
    } else {
      return Promise.reject(new Error("Circuit breaker is OPEN"));
    }
  }

  return fn()
    .then(function(result) {
      self.onSuccess();
      return result;
    })
    .catch(function(err) {
      self.onFailure();
      throw err;
    });
};

CircuitBreaker.prototype.onSuccess = function() {
  this.failureCount = 0;
  if (this.state === "HALF_OPEN") {
    this.successCount++;
    if (this.successCount >= 3) {
      this.state = "CLOSED";
      console.log("Circuit breaker closed");
    }
  }
};

CircuitBreaker.prototype.onFailure = function() {
  this.failureCount++;
  this.lastFailure = Date.now();
  if (this.failureCount >= this.failureThreshold) {
    this.state = "OPEN";
    console.warn("Circuit breaker opened after " + this.failureCount + " failures");
  }
};

module.exports = CircuitBreaker;
var CircuitBreaker = require("./circuitBreaker");

var dbBreaker = new CircuitBreaker({ failureThreshold: 5, resetTimeout: 30000 });

function queryDatabase(text, params) {
  return dbBreaker.execute(function() {
    return db.query(text, params);
  });
}

Common Issues and Troubleshooting

Application crashes with no error logged

An uncaught exception or unhandled rejection occurred without a handler:

Fix: Add process.on("uncaughtException") and process.on("unhandledRejection") handlers at application startup. These should log the error and trigger a graceful shutdown.

Express error middleware never runs

The error handler is not registered after routes, or route handlers are not passing errors to next:

Fix: Register error middleware with four parameters after all routes. Ensure async route handlers call next(err) when Promises reject. Use the asyncHandler wrapper for Promise-based routes.

Memory leaks from error retries

Unbounded retry loops accumulate in memory:

Fix: Set a maximum retry count. Use exponential backoff to reduce retry frequency. Track circuit breaker state to stop retries when a service is clearly down.

Generic "Internal server error" hides useful details

The error handler returns the same message for all 500 errors:

Fix: In development, include the full error message and stack trace. In production, log detailed errors server-side but return generic messages to clients. Use error codes to help clients identify specific error types.

Best Practices

  • Distinguish operational errors from programmer errors. Handle operational errors gracefully. Let programmer errors crash the process so you notice and fix them.
  • Always handle Promise rejections. Every .then() chain needs a .catch(). Use the asyncHandler wrapper for Express routes to catch rejections automatically.
  • Use custom error classes. Custom errors carry status codes, error codes, and operational flags that make error handling consistent and informative.
  • Implement graceful shutdown. When the process must exit, finish in-progress requests, close database connections, and flush logs before terminating.
  • Log errors with context. Include the request path, method, user ID, and any relevant parameters alongside the error message and stack trace.
  • Set timeouts on external calls. Database queries, HTTP requests, and file operations should all have timeouts. An operation that hangs indefinitely is worse than one that fails quickly.
  • Test error paths. Write tests that trigger errors — invalid input, missing resources, database failures. Error handling code that is never tested often has bugs of its own.
  • Use circuit breakers for external dependencies. When a dependency fails repeatedly, stop calling it temporarily. This prevents cascading failures and gives the dependency time to recover.

References

Powered by Contentful