How do you design custom error classes and propagate errors in Express?

3 minintermediatenodejscustom-errorsexpresserror-middlewarestatus-codes

Quick Answer

Extend the built-in Error class to add fields like statusCode and isOperational, so handlers can respond appropriately. In Express, throw/next these errors and centralize handling in a single error-handling middleware (four arguments) that maps them to HTTP responses.

Detailed Answer

Answer:

Custom error classes carry structured context beyond a message:

class AppError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.isOperational = true;          // distinguishes expected errors from bugs
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(resource) { super(`${resource} not found`, 404); }
}
class ValidationError extends AppError {
  constructor(msg) { super(msg, 400); }
}

Propagating in Express:

app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await db.getUser(req.params.id);
    if (!user) throw new NotFoundError('User');
    res.json(user);
  } catch (err) {
    next(err);          // hand off to the error middleware
  }
});

(An asyncHandler wrapper can remove the repetitive try/catch by catching rejections and calling next for you. Express 5 forwards async rejections automatically.)

Centralized error-handling middleware — note the four parameters (that's how Express recognizes it), registered last:

app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  // Log full details server-side; return a safe message to the client
  if (!err.isOperational) logger.error(err);
  res.status(status).json({ error: err.message });
});

Benefits: one place to format responses, log, and hide internal details; handlers stay focused on the happy path; the isOperational flag lets you decide what to expose vs. treat as a crash.