typescript 123 lines · 4 tabs

Backend: normalize errors with a single Express handler

Shared by codesnips Jan 2026
4 tabs
export class AppError extends Error {
  public readonly statusCode: number;
  public readonly code: string;
  public readonly isOperational: boolean;

  constructor(message: string, statusCode = 500, code = 'INTERNAL_ERROR', isOperational = true) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = isOperational;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class NotFoundError extends AppError {
  constructor(message = 'Resource not found') {
    super(message, 404, 'NOT_FOUND');
  }
}

export class BadRequestError extends AppError {
  constructor(message = 'Bad request', code = 'BAD_REQUEST') {
    super(message, 400, code);
  }
}

export interface FieldError {
  field: string;
  message: string;
}

export class ValidationError extends AppError {
  public readonly details: FieldError[];

  constructor(details: FieldError[], message = 'Validation failed') {
    super(message, 422, 'VALIDATION_ERROR');
    this.details = details;
  }
}
4 files · typescript Explain with highlit

This snippet shows how a Node/Express API can funnel every failure — thrown exceptions, rejected promises, validation errors, unknown 500s — through a single terminal error middleware so responses are consistent and logging happens in exactly one place. The central idea is to separate operational errors (a missing resource, a bad request the client caused) from programmer errors (a null dereference, an unexpected bug), and to render both as a stable JSON envelope.

In AppError the base class extends the native Error and carries an HTTP statusCode, a machine-readable code, and an isOperational flag. The subclasses NotFoundError, BadRequestError, and ValidationError just fix those fields, which keeps call sites terse: throwing new NotFoundError('user not found') is enough to produce a 404 with the right body. ValidationError additionally attaches a details array so field-level messages survive the trip to the handler. Setting this.name and calling Error.captureStackTrace preserves clean stack traces despite subclassing.

The asyncHandler wrapper solves a classic Express footgun: async route handlers that reject are not caught by Express, so an await that throws silently hangs the request. Wrapping a handler routes any rejection into next(err), which is what actually reaches the error middleware. Every async route should be wrapped rather than sprinkling try/catch everywhere.

errorHandler middleware is the single terminal handler, identified by its four-argument signature (err, req, res, next) — Express only treats it as an error handler with all four params present. It normalizes anything non-AppError into a generic 500 marked non-operational, logs unexpected errors with full stack while logging operational ones at a quieter level, and hides internal messages in production so implementation details never leak to clients. The code field gives frontends something to branch on that is more durable than parsing prose.

app wiring ties it together: routes throw freely, notFoundHandler converts unmatched paths into a NotFoundError, and errorHandler is registered last so it catches everything downstream. The trade-off is discipline — handlers must throw typed errors rather than calling res.status().json() ad hoc — but in return every error path is uniform, testable, and observable from one location.


Related snips

Share this code

Here's the card — post it anywhere.

Backend: normalize errors with a single Express handler — share card
Link copied