Express Error Handling

Robust error handling for Express apps: custom error classes, async wrappers, centralized error middleware, and environment-aware responses.

1. Custom Error Classes

// errors/AppError.js
class AppError extends Error {
  constructor(message, statusCode = 500, code = null) {
    super(message);
    this.name = 'AppError';
    this.statusCode = statusCode;
    this.code = code;          // e.g. 'INVALID_TOKEN'
    this.isOperational = true; // distinguishes known errors from bugs
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(resource = 'Resource') {
    super(`${resource} not found`, 404, 'NOT_FOUND');
  }
}

class ValidationError extends AppError {
  constructor(message, fields = {}) {
    super(message, 422, 'VALIDATION_ERROR');
    this.fields = fields;
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401, 'UNAUTHORIZED');
  }
}

module.exports = { AppError, NotFoundError, ValidationError, UnauthorizedError };

2. Async Error Handling

// Utility wrapper — no try/catch needed in routes
const asyncHandler = fn => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

// Or with a class for testing
class AsyncHandler {
  static wrap(fn) {
    return (req, res, next) =>
      Promise.resolve(fn(req, res, next)).catch(next);
  }
}

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

// Without wrapper — manual try/catch
router.get('/items/:id', async (req, res, next) => {
  try {
    const item = await Item.findById(req.params.id);
    res.json(item);
  } catch (err) {
    next(err); // must pass error to next()
  }
});

3. Centralized Error Middleware

// middleware/errorHandler.js
const { AppError } = require('../errors/AppError');

function errorHandler(err, req, res, next) {
  // Mongoose validation error
  if (err.name === 'ValidationError') {
    const fields = Object.fromEntries(
      Object.entries(err.errors).map(([k, v]) => [k, v.message])
    );
    return res.status(422).json({ error: 'Validation failed', fields });
  }

  // JWT error
  if (err.name === 'JsonWebTokenError') {
    return res.status(401).json({ error: 'Invalid token' });
  }

  // Operational / known errors
  if (err.isOperational) {
    const body = { error: err.message, code: err.code };
    if (err.fields) body.fields = err.fields;
    return res.status(err.statusCode).json(body);
  }

  // Unknown / programming errors
  console.error('UNHANDLED ERROR:', err);
  const message = process.env.NODE_ENV === 'production'
    ? 'Something went wrong'
    : err.message;
  res.status(500).json({ error: message });
}

module.exports = errorHandler;

// app.js — must be last middleware
app.use(errorHandler);

4. 404 Handler

// Place before errorHandler, after all routes
app.use((req, res, next) => {
  next(new NotFoundError(`Route ${req.method} ${req.path}`));
});

// Or send directly
app.use((req, res) => {
  res.status(404).json({
    error: 'Route not found',
    method: req.method,
    path: req.path,
  });
});

5. Unhandled Rejections & Exceptions

// Catch async errors that escape Express
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  // Graceful shutdown
  server.close(() => process.exit(1));
});

process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  process.exit(1); // always exit on uncaught exceptions
});

// Graceful shutdown on SIGTERM (e.g. Docker stop)
process.on('SIGTERM', () => {
  console.log('SIGTERM received — shutting down gracefully');
  server.close(() => {
    console.log('HTTP server closed');
    process.exit(0);
  });
});

6. HTTP Status Code Reference

CodeNameWhen to Use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST (new resource)
204No ContentSuccessful DELETE
400Bad RequestMalformed request / invalid input
401UnauthorizedAuthentication required
403ForbiddenAuthenticated but not authorized
404Not FoundResource does not exist
409ConflictDuplicate resource
422Unprocessable EntityValidation errors
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnexpected server error
503Service UnavailableServer temporarily down