← 返回题目列表

Node.js 服务如何做错误处理?

高频 中等 第 2 / 27 题 更新于 2026/07/27
Node.js错误处理异常日志

简化版

Node 服务要区分同步异常、异步 Promise rejection、业务错误和系统错误。接口层应统一错误处理中间件,记录日志、返回标准错误响应;进程级未捕获异常要监控并优雅退出,不建议吞掉继续运行。

详细版

Koa 示例:

app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = { message: err.message };
    ctx.app.emit('error', err, ctx);
  }
});

错误类型:

  • 参数错误:400。
  • 未登录/无权限:401/403。
  • 资源不存在:404。
  • 业务冲突:409。
  • 服务器异常:500。

日志要记录请求 id、用户 id、路径、堆栈、耗时等上下文。

完整版教学

一、错误处理要分层

业务代码可以 throw 业务错误,路由层不应该到处 try/catch 拼响应。更好的做法是在最外层中间件统一捕获,把错误转成标准响应。

这样接口格式一致,也方便接入日志和告警。

二、异步错误不能漏

Promise rejection 如果没有 await 或 catch,可能变成未处理拒绝。现代 Node 对 unhandled rejection 越来越严格,生产中必须监控。

Express 老版本对 async handler 的错误支持不如 Koa 自然,需要包装 async 函数或使用支持 Promise 的路由库。

三、进程级异常策略

uncaughtException 表示出现未捕获异常,进程状态可能已经不可信。通常做法是记录日志、停止接收新请求、等待存量请求结束,然后退出,由进程管理器拉起新进程。

不要简单 catch 后继续跑,这可能隐藏数据不一致问题。

四、面试追问与工程落地

常见追问是“如何避免错误信息泄露”。生产环境不要把完整 stack 返回给客户端,客户端只拿错误码和可读消息,详细堆栈进日志系统。

工程里还要配合请求链路 id。没有 request id,线上一个 500 很难从网关、Node 服务、数据库日志中串起来。

五、四种错误通道与传播边界

Node.js 的错误并不都走同一条通道,try/catch 只能捕获当前同步调用栈中的异常,不能包住一个稍后才执行的回调。Promise 错误要通过 await、返回 Promise 或 .catch() 接住;Node 风格回调把错误放在第一个参数;EventEmitter 则通常通过 error 事件报告错误。

错误来源正确接法容易遗漏的边界
同步函数try/catch只能覆盖当前调用栈
Promise / asyncawait + try/catch.catch()没有返回、没有等待的“悬空 Promise”
Node 回调检查 (err, data) 中的 err忘记 return 后继续执行成功分支
EventEmitter注册 error 监听器未监听的 error 事件会导致进程抛错退出

Express 5 会把路由处理器返回的 Promise rejection 或抛出的异常自动交给 next(error),Express 4 通常需要包装器;但两者都无法自动接住没有返回的后台 Promise。Koa 的洋葱模型依赖 await next(),外层中间件只有等待下游,才能统一捕获下游异步异常。

判断能否捕获错误,关键不是“外面有没有 try/catch”,而是错误发生时是否仍处在这条同步调用栈或被等待的 Promise 链上。

六、进程级故障与优雅退出

现代 Node.js 默认会把未处理的 Promise rejection 提升为未捕获异常;uncaughtExceptionunhandledRejection 因而都应该进入告警。uncaughtException 说明程序已经越过正常错误边界,处理器只适合做同步的最后日志和退出编排,不应把它当成恢复业务的方式继续运行。

优雅退出通常按以下顺序执行:

  1. 把 readiness 置为失败,停止接收新流量。
  2. 调用 server.close() 停止接收新连接并等待存量请求。
  3. 关闭数据库、消息队列和日志缓冲区等资源。
  4. 设置兜底超时,例如 30 秒后强制退出,避免进程永久挂住。
  5. 以非零状态退出,由容器或进程管理器重新拉起。

假设退出时还有 100 个请求,平均 500ms 内完成,直接 process.exit() 会同时截断它们;先摘流量并等待,通常只需约一个请求周期就能显著降低失败量。超时仍然必需,因为长连接、失控任务或不可用的下游可能让关闭过程永远无法结束。

错误对象与响应契约

可预期业务错误至少携带稳定的内部错误码、可公开消息、HTTP 状态和必要上下文,原始异常通过 cause 或日志字段保留。请求日志应关联 request id,但密码、令牌、Cookie、完整 SQL 参数等敏感数据必须脱敏。

统一处理器的决策顺序可以是:

  1. 判断响应头是否已经发送,避免二次写响应。
  2. 识别已知业务错误并映射稳定状态码。
  3. 未知错误统一返回通用 500,不暴露内部细节。
  4. 记录错误栈、cause、request id、路由和耗时。
  5. 按严重级别触发指标与告警,避免每个 4xx 都制造告警风暴。
  6. 对数据库连接、文件句柄等资源在 finally 或框架生命周期中清理。

错误码应面向客户端长期稳定,日志字段则面向排障丰富;把二者分开,才能同时兼顾兼容性、安全和可观测性。

七、常见误区与追问

  • 误区:在外层写一个 try/catch 就能捕获所有异步错误。 定时器回调、事件回调和未被等待的 Promise 已脱离当前同步调用栈,需要各自的错误通道。
  • 误区:生产环境的 500 响应可以直接返回 err.message 和堆栈。 内部错误可能泄露路径、SQL 和依赖信息,应返回稳定错误码,把完整上下文写入受控日志。
  • 误区:监听 uncaughtException 后进程就可以继续服务。 未捕获异常可能已经破坏内存状态或业务不变量,可靠做法是记录、优雅停止并重启。
  • 追问:Express 5 对 async handler 做了什么改进? 处理器返回的 Promise 被拒绝或抛错时会自动调用 next(error),但悬空的异步任务仍需自行捕获。
  • 追问:为什么 EventEmitter 的 error 事件要单独监听? 没有监听器时它不是普通业务事件,Node 会抛出该错误并可能终止进程。
  • 追问:业务错误和程序错误如何区分? 参数不合法、资源冲突等预期错误可映射为稳定状态码;空指针、断言失败等程序错误应告警并修复,不能伪装成正常业务分支。
  • 追问:响应头已经发送后发生错误怎么办? 不能再改状态码或重复发送 JSON,应停止或销毁当前响应,并让框架默认错误处理或连接关闭逻辑完成清理。

八、加强记忆

Node 错误处理记住“三层”:业务层产生有类型的错误,框架层统一映射响应,进程层对越界故障告警并优雅退出。再按同步、Promise、回调、事件四种通道检查传播链,尤其排查没有 returnawait 的悬空任务。不要吞错,也不要把内部堆栈暴露给用户。