NodeJSCentralized Error Handling

Centralized Error Handling

In Express, a 4-argument middleware(err, req, res, next) — is the error handler. Place it after all routes and it catches any error forwarded by next(err), any error thrown inside a synchronous handler, and (with an asyncHandler wrapper) any rejected Promise from async handlers. One function receives every unhandled error, classifies it, logs it, and sends a consistent response. This page builds that handler and shows how to wire it into a real app.

The 4-argument signature

JS
// Express identifies an error handler by EXACTLY 4 arguments:
app.use((err, req, res, next) => {
  // ...
})

// ❌ Three arguments — this is a regular middleware, not an error handler:
app.use((req, res, next) => { /* never called for errors */ })
The error handler MUST have exactly 4 parameters — even if you don't use `next`
Express checks the function's `.length` property to identify error-handling middleware. If you define `(err, req, res)` (three args) it's treated as regular middleware, errors skip right past it, and Express's default error handler takes over — sending a plain text 500 with the stack trace visible. Always declare all four parameters, even if `next` goes unused. And register error middleware **after** all routes and regular middleware.
A production-ready error handler

middleware/errorHandler.js

JS
export function errorHandler(err, req, res, next) {
  // Default to 500 if no statusCode was set (programmer error / unexpected)
  const statusCode = err.statusCode ?? 500
  const isOperational = statusCode < 500

  // Log with full detail — server-side only:
  if (isOperational) {
    logger.warn({ err, reqId: req.id, path: req.path }, 'Operational error')
  } else {
    logger.error({ err, reqId: req.id, path: req.path, body: req.body }, 'Unhandled error')
  }

  // Send a safe, structured response to the client:
  res.status(statusCode).json({
    error: isOperational ? err.message : 'An unexpected error occurred',
    code:  isOperational ? err.code    : 'INTERNAL_ERROR',
    ...(isOperational && err.details ? { details: err.details } : {}),
    requestId: req.id,          // lets the user quote it to support
  })
}
Log everything server-side; send the client only what's safe
The handler logs `err` (which includes the stack trace), request context, and for programmer errors even the request body — all on the server. The client response contains only a message (safe for operational errors, generic for unexpected ones), a machine-readable `code`, and a `requestId` the user can quote. This allows debugging without leaking internals. Never include `err.stack` in the response body.
Registering the handler

app.js

JS
import express from 'express'
import { router } from './routes/index.js'
import { errorHandler } from './middleware/errorHandler.js'
import { notFoundHandler } from './middleware/notFoundHandler.js'

const app = express()

app.use(express.json())
// ...other global middleware...

app.use('/api', router)          // all application routes

// 404 for unknown routes — BEFORE the error handler:
app.use(notFoundHandler)

// Error handler — LAST:
app.use(errorHandler)

middleware/notFoundHandler.js

JS
import { NotFoundError } from '../errors/index.js'

export function notFoundHandler(req, res, next) {
  next(new NotFoundError(`Route ${req.method} ${req.path} not found`))
}
Throwing from routes — the full chain

JS
import { asyncHandler } from '../utils/asyncHandler.js'
import { NotFoundError, ValidationError } from '../errors/index.js'

router.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.users.findById(req.params.id)
  if (!user) throw new NotFoundError('User')    // → errorHandler receives this
  res.json(user)
}))

// Synchronous throw also works — Express catches it:
router.get('/sync', (req, res) => {
  throw new ValidationError('Bad input')        // → errorHandler receives this
})
Validation errors with field details

JS
// A ValidationError with per-field details:
throw new ValidationError('Invalid request body', [
  { field: 'email', message: 'Must be a valid email address' },
  { field: 'age',   message: 'Must be a positive integer' },
])

// The handler sends this (statusCode = 400):
{
  "error": "Invalid request body",
  "code": "VALIDATION_ERROR",
  "details": [
    { "field": "email", "message": "Must be a valid email address" },
    { "field": "age",   "message": "Must be a positive integer" }
  ],
  "requestId": "abc-123"
}
Checklist
  • Exactly 4 args on the error handler function.

  • Registered last — after all routes and other middleware.

  • 404 handler before the error handler — routes that don't match need a next(new NotFoundError(...)).

  • Never expose stack traces to the client.

  • Include a requestId so users can report issues that correlate with server logs.

  • Log operational and programmer errors differently — warn vs error severity.

Next
Handle the nuclear option — process-level crashes: [Uncaught Exceptions](/nodejs/uncaught-exceptions).