← 中文资源

简体中文版本

Express 类型化错误中间件

在 TypeScript 里集中处理 Express 错误:AppError 类型层级带 kind 判别字段,switch 穷尽映射状态码,未知错误统一 500 兜底。

阅读英文原文

示例说明

TypeScript 给错误处理带来两个杀手锏:判别字段与穷尽检查。AppError 的 kind 是字面量联合,statusFor 的 switch 覆盖全部分支且没有 default——新增错误种类时漏写分支会直接编译报错。中间件先 instanceof 收窄类型,业务错误按 kind 映射状态码,非预期错误记日志后回笼统响应不泄漏内部细节。

技术要点

  • kind 判别联合支撑穷尽 switch
  • wrap 桥接 Promise 拒绝与 next
  • instanceOf 收窄未知错误类型

代码解读

wrap 的高阶函数把异步 handler 的异常转交错误链;示例路由对 id 为 0 抛出 not-found 错误。错误中间件里非 AppError 实例走兜底分支只记堆栈,AppError 则映射状态码并回显 kind,客户端可按类别做差异化提示。

使用建议

错误消息要区分对内对外:对外文案克制、对内日志详尽。新增 kind 时利用编译器穷尽提示补全映射。生产环境给 5xx 加告警钩子。

代码示例

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);