Express Error Handling Typed Middleware

Centralize Express error handling in TypeScript with a typed AppError hierarchy, status mapping, and an exhaustively narrowed middleware.

TypescriptNodeResilience

Code

snippet
import express, { NextFunction, Request, Response } from 'express';

type ErrorKind = 'validation' | 'not-found' | 'conflict' | 'internal';

class AppError extends Error {
  constructor(
    readonly kind: ErrorKind,
    message: string,
    readonly status = 500,
  ) {
    super(message);
  }
}

const statusFor = (kind: ErrorKind): number => {
  switch (kind) {
    case 'validation': return 400;
    case 'not-found': return 404;
    case 'conflict': return 409;
    case 'internal': return 500;
  }
};

const wrap = (fn: (req: Request, res: Response) => Promise<unknown>) =>
  (req: Request, res: Response, next: NextFunction) => {
    fn(req, res).catch(next);
  };

const app = express();
app.use(express.json());

app.get('/items/:id', wrap(async (req, res) => {
  if (req.params.id === '0') {
    throw new AppError('not-found', 'item 0 does not exist');
  }
  res.json({ id: req.params.id });
}));

app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
  if (!(err instanceof AppError)) {
    console.error('unexpected:', err);
    res.status(500).json({ error: 'internal error' });
    return;
  }
  const status = statusFor(err.kind);
  if (status >= 500) console.error('server error:', err.message);
  res.status(status).json({ error: err.message, kind: err.kind });
});

app.listen(3000);