服务端与数据 / 进阶
Express 异步错误处理:让异常只经过一个出口
把业务异常、参数错误和未知故障收敛到统一中间件,避免重复 try/catch、泄露堆栈和一条请求记录多次日志。
先区分可预期错误和程序故障
资源不存在、库存不足和参数不合法属于可预期的业务结果;数据库连接中断、空指针和断言失败属于程序故障。前者需要稳定的错误码,后者需要完整日志,但客户端都不应该看到内部堆栈。
EXAMPLE / 01Express
export class AppError extends Error {
constructor(message, { status = 500, code = 'INTERNAL_ERROR', details } = {}) {
super(message);
this.name = 'AppError';
this.status = status;
this.code = code;
this.details = details;
}
}路由只负责抛出错误
在 Express 5 中,被 async 路由拒绝的 Promise 会自动交给错误中间件。Express 4 则需要包装器调用 next。无论哪个版本,都不要在每个路由里重复记录和返回错误。
EXAMPLE / 02Express
app.get('/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
throw new AppError('用户不存在', { status: 404, code: 'USER_NOT_FOUND' });
}
res.json({ data: user });
});在最后一个中间件统一响应
错误中间件记录完整异常;上层网关只记录状态、耗时和 requestId。这样既保留上下文,也不会让同一故障生成多条重复告警。
EXAMPLE / 03Express
app.use((err, req, res, next) => {
if (res.headersSent) return next(err);
const known = err instanceof AppError;
const status = known ? err.status : 500;
req.log[status >= 500 ? 'error' : 'warn']({ err, requestId: req.id });
res.status(status).json({
error: {
code: known ? err.code : 'INTERNAL_ERROR',
message: known ? err.message : '服务暂时不可用',
requestId: req.id,
...(known && err.details ? { details: err.details } : {})
}
});
});