Express Cheatsheet

Error Handling

Use this Express reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

How Express Error Handling Works

Any middleware or route handler that calls next(err) (with a truthy argument) bypasses remaining regular middleware and jumps to the nearest error-handling middleware — one with exactly four parameters: (err, req, res, next).

Triggering Errors

// Synchronous throw — Express catches this in routes (NOT in async code)
app.get('/sync', (req, res) => {
  throw new Error('Sync error'); // caught by Express 4 for sync handlers only
});

// next(err) — works everywhere, always preferred
app.get('/data', async (req, res, next) => {
  try {
    const data = await db.query();
    res.json(data);
  } catch (err) {
    next(err); // forward to error handler
  }
});

// Pass an HTTP status with the error
app.get('/secret', (req, res, next) => {
  const err = new Error('Forbidden');
  err.status = 403;
  next(err);
});

Express 4: Async errors from async functions are NOT auto-caught — you must use try/catch + next(err) or an async wrapper. Express 5: Rejected promises are forwarded to next(err) automatically.

Error-Handling Middleware

Must be registered after all routes and other middleware. Must have exactly 4 parameters.

// Minimal error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: {
      message: err.message,
      ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
    },
  });
});

Custom Error Classes

class AppError extends Error {
  constructor(message, status = 500, code = 'INTERNAL_ERROR') {
    super(message);
    this.name = 'AppError';
    this.status = status;
    this.code   = code;
  }
}

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

class ValidationError extends AppError {
  constructor(details) {
    super('Validation failed', 400, 'VALIDATION_ERROR');
    this.details = details;
  }
}

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

Using them in routes:

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

Production Error Handler

app.use((err, req, res, next) => {
  // Log every error with context
  console.error({
    message: err.message,
    status:  err.status,
    stack:   err.stack,
    url:     req.originalUrl,
    method:  req.method,
    ip:      req.ip,
  });

  // Operational errors: known, expected
  if (err instanceof AppError) {
    return res.status(err.status).json({
      error: {
        code:    err.code,
        message: err.message,
        ...(err.details && { details: err.details }),
      },
    });
  }

  // Programmer/unexpected errors: don't leak details
  res.status(500).json({ error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' } });
});

Async Handler Wrapper (Express 4)

Eliminates repetitive try/catch blocks.

const asyncHandler = fn => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

// Usage
app.get('/users', asyncHandler(async (req, res) => {
  const users = await db.users.findAll();
  res.json(users);
}));

// Alternative: wrap all routes in a router
const asyncRouter = express.Router();
['get', 'post', 'put', 'patch', 'delete'].forEach(method => {
  const original = asyncRouter[method].bind(asyncRouter);
  asyncRouter[method] = (path, ...handlers) => {
    original(path, ...handlers.map(h =>
      h.length === 4 ? h : (req, res, next) => Promise.resolve(h(req, res, next)).catch(next)
    ));
  };
});

Chaining Multiple Error Handlers

Use multiple error-handling middleware for separation of concerns.

// 1. Handle Multer file upload errors
app.use((err, req, res, next) => {
  if (err.code === 'LIMIT_FILE_SIZE') {
    return res.status(413).json({ error: 'File too large' });
  }
  next(err);
});

// 2. Handle database constraint errors (e.g., pg)
app.use((err, req, res, next) => {
  if (err.code === '23505') { // unique_violation in PostgreSQL
    return res.status(409).json({ error: 'Resource already exists' });
  }
  next(err);
});

// 3. Final catch-all
app.use((err, req, res, next) => {
  res.status(err.status || 500).json({ error: err.message });
});

404 Handler

A 404 is not an error — it's a successful response with no match. Register a regular middleware after all routes.

// 404 — after all routes, before error handler
app.use((req, res, next) => {
  res.status(404).json({
    error: { code: 'NOT_FOUND', message: `Cannot ${req.method} ${req.path}` },
  });
});

// Or: create an error and forward it to the error handler
app.use((req, res, next) => {
  next(new NotFoundError(`Route ${req.method} ${req.path}`));
});

Calling next() in Error Handlers

app.use((err, req, res, next) => {
  if (res.headersSent) {
    return next(err); // delegate to default Express error handler if headers already sent
  }
  res.status(500).json({ error: err.message });
});

Never call next(err) after you've already sent a response — it causes the "headers already sent" error.

Common Error Patterns

ScenarioStatusPattern
Missing auth token401next(new UnauthorizedError())
Valid token, wrong role403next(new AppError('Forbidden', 403, 'FORBIDDEN'))
Resource not found404next(new NotFoundError('User'))
Duplicate unique key409Catch db error, rethrow as 409
Body too large413Caught automatically by express.json size limit
Validation failure400/422next(new ValidationError(errors.array()))
Unhandled promise500Use async wrapper; Express 5 catches automatically
External API failure502/503Catch and rethrow with appropriate status

Logging Errors

const winston = require('winston');

app.use((err, req, res, next) => {
  winston.error({
    message: err.message,
    level:   err.status < 500 ? 'warn' : 'error',
    status:  err.status,
    method:  req.method,
    path:    req.originalUrl,
    ip:      req.ip,
    userId:  req.user?.id,
  });
  res.status(err.status || 500).json({ error: err.message });
});