Skip to content

HTTP Error Classes

A class hierarchy for HTTP errors with automatic status codes, default messages from Node's STATUS_CODES, and support for attaching extra details. Throw structured errors that Express error handlers can inspect by status.

When to Use

  • Throwing errors that map to specific HTTP status codes
  • Building Express error-handling middleware that switches on error.status
  • Attaching structured details (field names, limits, context) to errors
  • Distinguishing client errors (4xx) from server errors (5xx) in catch blocks

The Pattern

javascript
import { STATUS_CODES } from "node:http";

class HttpError extends Error {
  static status = 500;
  status = 500;
  
  constructor(message, extras = {}) {
    super(message);
    Object.assign(this, extras);
    if (!this.message) {
      this.message = STATUS_CODES[this.status];
    }
  }
}

class ClientError extends HttpError {
  static status = 400;
  status = 400;
}

class UnauthorizedError extends ClientError {
  static status = 401;
  status = 401;
}

class ForbiddenError extends ClientError {
  static status = 403;
  status = 403;
}

class NotFoundError extends ClientError {
  static status = 404;
  status = 404;
}

class ServerError extends HttpError {
  static status = 500;
  status = 500;
}

Hierarchy

HttpError (500)
├── ClientError (400)
│   ├── UnauthorizedError (401)
│   ├── ForbiddenError (403)
│   └── NotFoundError (404)
└── ServerError (500)

Design decisions

  • Both static and instance status. The static property enables UnauthorizedError.status === 401 checks without instantiation. The instance property enables error.status on thrown instances.
  • STATUS_CODES fallback. If no message is passed, the error uses Node's built-in status text ("Not Found", "Forbidden", etc.).
  • extras object. Additional properties are merged onto the error instance, so handlers can access error.field, error.limit, etc.

Throwing errors

Throw with no message to use Node's built-in status text as the default:

javascript
throw new NotFoundError();
// error.message === "Not Found"
// error.status  === 404

Pass a custom message to override the default:

javascript
throw new UnauthorizedError("Invalid token");
// error.message === "Invalid token"
// error.status  === 401

Attach structured details through the extras parameter for handlers to inspect:

javascript
throw new ClientError("Validation failed", {
  field: "email",
  reason: "Invalid format",
});
// error.field  === "email"
// error.reason === "Invalid format"
// error.status === 400

Express error handler

An Express error-handling middleware can branch on instanceof to separate client errors from server errors:

javascript
app.use((error, req, res, next) => {
  if (error instanceof ClientError) {
    res.status(error.status).json({
      error: error.message,
      ...error,
    });
  } else {
    res.status(500).json({
      error: "Internal Server Error",
    });
  }
});

Checking error category

Use instanceof with parent classes to catch broad categories without listing individual status codes:

javascript
try {
  await fetchResource(id);
} catch (error) {
  if (error instanceof ClientError) {
    // 4xx: client's fault
  } else if (error instanceof ServerError) {
    // 5xx: our fault
  }
}

Trade-offs

ApproachProsCons
Class hierarchyType-safe, instanceofMore classes
Plain objectsSimpleNo stack trace
String codesFlat, serializableNo hierarchy

The hierarchy is the key value. Catching ClientError catches all 4xx errors. Catching HttpError catches everything. This lets error handlers be as broad or narrow as needed without listing individual status codes.