第01章:AI生成的Express应用没有错误处理中间件

第01章:AI生成的Express应用没有错误处理中间件

“AI 帮你写了 Express 应用,功能都对,运行也没问题。直到生产环境出了一个未处理的异常,进程崩溃了。PM2 帮你重启了,但在重启的那几秒里,所有用户都看到了 502。或者数据库查询抛出了异常,客户端收到了一个包含完整 stack trace 的响应——暴露了你用的是什么数据库、版本号、文件路径。AI 不会主动加错误处理,它认为功能代码是优先的。”


ℹ️ 版本说明:本章基于 Node.js v24.16.0 + Express 5.x

1.1 AI默认会生成什么

// AI 通常给你的 Express 应用(没有错误处理)
const express = require('express');
const app = express();

app.get('/api/users/:id', async (req, res) => {
  const user = await db.findById(req.params.id);  // 如果抛出异常呢?
  res.json(user);
});

app.listen(3000, () => console.log('Server running on port 3000'));

// 如果 db.findById 抛出异常:
// - Express 4.x:进程崩溃(没有 catch)
// - Express 5.x:自动传给下一个 error handler(但你没有配置 error handler)
// 两种情况都不理想

1.2 AI通常遗漏的4个坑

⚠️ 坑1:Express 需要4个参数的错误中间件

Express 通过中间件参数数量区分"普通中间件"和"错误处理中间件"——必须有4个参数 (err, req, res, next),否则 Express 不认为它是错误处理器:

// ❌ 3个参数:这是普通中间件,不会处理错误
app.use((req, res, next) => {
  res.status(500).json({ error: 'something went wrong' });
});

// ✅ 4个参数:这才是错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: process.env.NODE_ENV === 'production' 
      ? 'Internal Server Error'    // 生产环境不暴露细节
      : err.message                // 开发环境显示详情
  });
});

位置重要:错误处理中间件必须在所有路由之后注册。


⚠️ 坑2:async 路由里的异常不会自动传给错误处理器(Express 4.x)

// Express 4.x:async 函数里的 throw 不会被 Express 捕获
app.get('/api/users', async (req, res) => {
  const users = await db.query('SELECT * FROM users'); // 如果 throw,进程崩溃
  res.json(users);
});

// 修复方案1:手动 try-catch(繁琐)
app.get('/api/users', async (req, res, next) => {
  try {
    const users = await db.query('SELECT * FROM users');
    res.json(users);
  } catch (err) {
    next(err);  // 传给错误处理中间件
  }
});

// 修复方案2:封装 asyncWrapper(更优雅)
const asyncWrapper = (fn) => (req, res, next) => fn(req, res, next).catch(next);

app.get('/api/users', asyncWrapper(async (req, res) => {
  const users = await db.query('SELECT * FROM users');
  res.json(users);
}));

// 修复方案3:升级到 Express 5.x(自动处理 async 路由异常)

⚠️ 坑3:错误响应暴露内部信息

// 危险:把完整错误信息返回给客户端
app.use((err, req, res, next) => {
  res.status(500).json({ 
    error: err.message,   // 可能包含 SQL 查询、文件路径
    stack: err.stack      // 完整调用栈,泄露代码结构
  });
});

// 安全:生产环境只返回通用错误信息
app.use((err, req, res, next) => {
  // 记录完整错误(内部日志)
  logger.error({ err, url: req.url, method: req.method });
  
  // 返回给客户端的只有通用信息
  const statusCode = err.status || err.statusCode || 500;
  res.status(statusCode).json({
    error: statusCode < 500 ? err.message : 'Internal Server Error',
    requestId: req.id  // 用于关联日志,方便客户支持查询
  });
});

⚠️ 坑4:进程级别的未捕获异常

即使有 Express 错误处理中间件,有些情况仍然会让进程崩溃:

// 这些异常不会被 Express 捕获
setTimeout(() => {
  throw new Error('来自 setTimeout 的未捕获异常');
}, 1000);

// 需要在进程级别捕获
process.on('uncaughtException', (err) => {
  logger.fatal({ err }, 'Uncaught Exception');
  // 记录错误后主动退出(uncaughtException 后进程状态不可信)
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  logger.error({ reason }, 'Unhandled Rejection');
  // 可以选择不退出(视情况而定)
});

1.3 更好的提示词

提示词 P01:生产级 Express 错误处理

使用时机:为 Express 应用添加完整的错误处理体系

帮我为一个 Node.js v24.16.0 + Express 5.x 应用添加完整的生产级错误处理。

应用情况:
- 有多个 async/await 路由
- 连接 PostgreSQL 数据库
- 需要区分开发环境和生产环境的错误响应

需要配置的内容:

1. 自定义错误类(区分 4xx 客户端错误和 5xx 服务器错误):
   class AppError extends Error {
     constructor(message, statusCode) { ... }
   }
   class ValidationError extends AppError { ... }
   class NotFoundError extends AppError { ... }

2. Express 错误处理中间件(4个参数,放在所有路由之后):
   - 记录错误日志(包含 requestId、url、method)
   - 生产环境:只返回通用错误信息
   - 开发环境:返回完整 stack trace
   - 特殊处理 Joi/Zod 验证错误(转为 400)
   - 特殊处理 Prisma/Sequelize 唯一约束错误(转为 409)

3. 进程级别的异常处理:
   process.on('uncaughtException', ...)
   process.on('unhandledRejection', ...)

4. Express 5.x 和 4.x 的差异(async 路由处理)

5. 如何测试错误处理是否工作:
   - 手动 throw 一个错误
   - 模拟数据库连接失败
   - 验证客户端不会看到 stack trace

基于 Node.js v24.16.0 + Express 5.x。

提示词 P02:统一错误格式和请求追踪

使用时机:多个微服务,需要统一的错误响应格式和日志追踪

帮我设计一套统一的错误响应格式和请求追踪机制。

需求:
- 每个请求有唯一的 requestId(方便客服根据用户反馈查日志)
- 所有错误响应格式统一(方便前端统一处理)
- 错误日志包含足够信息(requestId、user、url、耗时)

1. requestId 中间件(每个请求注入唯一 ID):
   - 优先使用请求头里的 X-Request-ID(来自 API Gateway)
   - 没有则生成 UUID
   - 把 requestId 挂在 req 对象上

2. 统一错误响应格式:
   {
     "success": false,
     "error": {
       "code": "USER_NOT_FOUND",
       "message": "用户不存在",
       "requestId": "abc-123"
     }
   }

3. pino 日志(结构化日志,包含所有上下文):
   - 每个请求完成时记录:method、url、statusCode、duration、userId
   - 错误时记录:完整 err 对象、requestId

4. 示例:如何在路由里抛出带 code 的错误,最终记录到日志并返回给客户端?

基于 Node.js v24.16.0 + Express + pino。

提示词 P03:graceful shutdown(优雅关闭)

使用时机:部署时 Node.js 进程需要优雅关闭(不丢失进行中的请求)

帮我为 Node.js v24.16.0 Express 应用配置优雅关闭。

场景:
- 部署时会重启进程(PM2 reload 或 Docker 重启)
- 希望当前正在处理的请求完成后再关闭
- 关闭前需要关闭数据库连接池

优雅关闭步骤:
1. 捕获 SIGTERM 信号(PM2/Docker 发送)
2. 停止接受新连接(server.close())
3. 等待已有连接上的请求完成
4. 关闭数据库连接池(PostgreSQL pg pool)
5. 退出进程

注意事项:
- Node.js 的 server.close() 只停止新连接,但不会终止保活的长连接
- 如何处理 keepalive 连接?(设置 Connection: close 响应头)
- 设置超时:30秒内如果没有关闭完成,强制 process.exit(1)

给我完整的 graceful shutdown 代码。

基于 Node.js v24.16.0 + Express 5.x + pg(PostgreSQL)。

1.4 验收清单

检查项 验证方法 AI辅助
有4参数错误处理中间件 代码里有 (err, req, res, next) 用 P01 添加
错误中间件在所有路由之后 检查中间件注册顺序 用 P01 审查代码结构
生产环境不返回 stack trace 向 /api/error 发请求,响应不含 stack 用 P01 配置环境判断
有 requestId(便于追踪) 错误响应里有 requestId 用 P02 添加追踪
process 级别异常处理 代码里有 uncaughtException 监听 用 P01 添加
有优雅关闭逻辑 SIGTERM 时等待请求完成 用 P03 添加

1.5 本章小结

如果你只记一件事:在所有路由注册之后,加上 Express 的4参数错误处理中间件,并根据环境变量区分开发和生产的响应格式——开发环境返回完整 stack trace 方便调试,生产环境只返回通用错误信息防止信息泄露。

Node.js 错误处理的三个层次

  1. Express 错误处理中间件(路由层):捕获同步错误和 Express 5.x 的 async 路由错误,统一格式响应
  2. asyncWrapper / try-catch(应用层):Express 4.x 的 async 路由必须手动 catch 传给 next
  3. process 级别异常处理(进程层):uncaughtException 和 unhandledRejection,确保异常记录日志后优雅退出

→ 第2章:AI帮我用了async/await但没有处理未捕获的Promise rejection