Custom Errors

Custom Error Classes and Operational Errors

Node's own errors carry a code; yours should too. The distinction that matters in a server is between operational errors — a bad request body, a 404, an upstream timeout, things a request handler can answer — and programmer errors, which are bugs. Handle the first kind; let the second crash the process, because the state is no longer trustworthy (Unhandled Rejections).

One base class gives every error in your codebase a code, an HTTP status and that flag:

errors.mjs — a base class for application errorsJavaScript
export class AppError extends Error {
  constructor(message, { code, status = 500, cause } = {}) {
    super(message, { cause });
    this.name = new.target.name;
    this.code = code ?? 'ERR_APP';
    this.status = status;
    this.isOperational = true;
    Error.captureStackTrace(this, new.target);
  }
  toJSON() {
    return { name: this.name, code: this.code, status: this.status, message: this.message };
  }
}
export class NotFoundError extends AppError {
  constructor(what, id) {
    super(`${what} ${id} not found`, { code: 'ERR_NOT_FOUND', status: 404 });
  }
}

Three details carry the design. new.target is the constructor actually invoked with new, so name becomes "NotFoundError" without every subclass repeating itself. Error.captureStackTrace(this, new.target) removes the constructor frames. And toJSON controls what a JSON response serializes — stack is deliberately absent, so an accidental res.json(err) cannot leak your directory layout.

Using the classesJavaScript
const e = new NotFoundError('order', 'A-1042');
console.log(JSON.stringify(e), '\n' + e.stack.split('\n')[1].trim());
Output
{"name":"NotFoundError","code":"ERR_NOT_FOUND","status":404,"message":"order A-1042 not found"}
at file:///srv/shop/app.mjs:19:11

e passes instanceof for NotFoundError, AppError and Error alike, and its stack starts at line 19 of app.mjs — the new NotFoundError(...) call — not inside errors.mjs. Because the base class forwards cause, a sibling class for upstream failures needs only super(msg, { code: 'ERR_UPSTREAM', status: 502, cause }). Express.js wires these classes into Express 24,430 error middleware, where isOperational and status let one handler answer 404 and 502 and rethrow the rest.