错误处理与健壮性
Node.js 错误处理与健壮性
构建健壮的 Node.js 应用,必须系统性地处理各种错误,包括同步、异步、未捕获异常等,并结合进程管理和健康检查保证服务可用性。
一、错误类型
Node.js 内置了多种错误构造函数,所有错误都继承自 Error。
| 类型 | 描述 | 常见场景 |
|---|---|---|
Error | 通用错误 | 主动抛出的未知错误 |
TypeError | 参数类型不符合预期 | 调用非函数、读取 null 属性 |
RangeError | 数值超出有效范围 | 数组长度负值、递归栈溢出 |
SyntaxError | 语法错误(通常发生在启动解析阶段) | 代码书写错误(eval 也可能触发) |
ReferenceError | 引用未声明变量 | 访问未定义的变量 |
URIError | 全局 URI 处理函数错误 | decodeURI 传入非法字符串 |
自定义 AppError:为业务错误统一结构,便于分类处理。
class AppError extends Error {
constructor(message, statusCode, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational; // 区分可恢复错误 vs 程序缺陷
Error.captureStackTrace(this, this.constructor);
}
}
使用示例:
throw new AppError("用户不存在", 404);
二、同步与异步错误捕获
1. try/catch 仅适用于同步代码
try {
JSON.parse("invalid json");
} catch (err) {
console.error("同步错误已捕获", err.message);
}
对于异步回调中的错误,try/catch 无法捕获,因为回调执行时 try 块已结束。
2. 异步错误捕获方法
Promise 链:必须添加 .catch()。
doSomethingAsync()
.then(result => process(result))
.catch(err => console.error("Promise 错误:", err));
async/await:使用 try/catch 包裹 await。
async function run() {
try {
const data = await fetchData();
console.log(data);
} catch (err) {
console.error("async 错误:", err);
}
}
回调风格:错误优先回调的第一个参数为错误对象。
fs.readFile("file.txt", (err, data) => {
if (err) {
console.error("读取失败", err);
return;
}
// 处理 data
});
三、全局未捕获异常
1. process.on('uncaughtException')
该事件用于捕获未在 try/catch 或 .catch 中处理的同步/异步错误(例如 setTimeout 里的同步错误、未捕获的异常)。它只是最后防线,不应依赖它来维持应用正常运行,因为出错后应用可能处于不一致状态。
process.on("uncaughtException", err => {
console.error("未捕获异常:", err);
// 执行清理逻辑(关闭数据库连接、服务器等)
// 然后优雅退出
process.exit(1);
});
最佳实践:记录错误后立即退出进程(process.exit(1)),并让进程管理器(如 PM2)负责重启。
2. process.on('unhandledRejection')
用于捕获未处理的 Promise 拒绝(没有 .catch 或 try/catch 的 await)。
process.on("unhandledRejection", (reason, promise) => {
console.error("未处理的 Promise 拒绝:", reason);
// 可选择退出进程
process.exit(1);
});
在 Node.js 15+ 中,未处理的拒绝会触发 unhandledRejection,但默认不会终止进程(需要显式 process.exit)。建议配合 --unhandled-rejections=strict 启动参数,使其直接抛出未捕获异常。
四、进程退出码
process.exitCode 用于设置进程退出时的状态码,约定如下:
| 退出码 | 含义 |
|---|---|
| 0 | 成功完成 |
| 1 | 未捕获的错误(通用错误) |
| 2 | Bash 内置命令使用错误(很少用于 Node) |
| 3 | 内部 JavaScript 解析错误(启动阶段) |
| 4 | 内部执行失败(如 EvalError) |
| 5 | 致命错误(V8 内部断言失败) |
| 6 | 非函数异常处理(事件循环中抛出的异常) |
| 7 | 异常处理时再抛出异常(很少见) |
| 9 | 无效参数 |
| 12 | 无效调试参数 |
| 128 + 信号码 | 被信号终止(如 SIGKILL = 128+9=137) |
设置方式:
// 方法1:设置 exitCode 并让进程自然退出
process.exitCode = 1;
// 方法2:立即退出(注意会中断尚未完成的 I/O)
process.exit(1);
推荐使用 process.exitCode 并让事件循环清空,避免强制退出导致数据丢失。
五、健康检查与自动重启
生产环境中应使用进程管理器或系统守护工具,实现崩溃自动重启和健康检查。
1. PM2(常用 Node.js 进程管理器)
# 启动应用
pm2 start app.js --name my-app
# 启用自动重启(默认开启)
pm2 start app.js --watch # 监听文件变化重启
# 设置内存限制,超出自动重启
pm2 start app.js --max-memory-restart 500M
# 查看状态
pm2 list
pm2 monit
PM2 会监控进程,当进程退出(非 0 码)时自动重启。可在 ecosystem.config.js 中配置健康检查:
module.exports = {
apps: [
{
name: "app",
script: "app.js",
instances: 1,
exec_mode: "fork",
max_memory_restart: "500M",
// 自定义健康检查脚本
health_check: "/health",
// 重启策略
min_uptime: "10s",
max_restarts: 10,
},
],
};
2. systemd(Linux 系统服务)
创建 /etc/systemd/system/myapp.service:
[Unit]
Description=My Node.js App
After=network.target
[Service]
Type=simple
User=node
WorkingDirectory=/home/node/app
ExecStart=/usr/bin/node app.js
Restart=on-failure
RestartSec=10s
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
启用并启动:
systemctl enable myapp
systemctl start myapp
3. 健康检查 API 实现
应用本身应提供一个 HTTP 端点(如 /health),返回 200 表示正常。
app.get("/health", (req, res) => {
// 检查数据库、缓存等关键依赖状态
const isHealthy = checkDependencies();
res.status(isHealthy ? 200 : 503).json({ status: isHealthy ? "up" : "down" });
});
Kubernetes 等编排系统可基于此端点进行存活探针(livenessProbe)和就绪探针(readinessProbe)。
六、最佳实践总结
- 始终捕获异步错误:使用
async/await+try/catch,或 Promise.catch。 - 定义业务错误类:继承
Error并添加状态码、isOperational标志,与非操作错误(程序缺陷)区分。 - 注册全局处理器:
uncaughtException和unhandledRejection用于记录日志并优雅退出,而非维持运行。 - 合理设置退出码:表示失败原因,便于进程管理器判断是否需要重启。
- 使用进程管理器:PM2 或 systemd 实现自动重启、日志管理和资源限制。
- 实现健康检查端点:暴露
/health或/ready,让外部系统了解服务状态。
通过以上措施,可以构建高可靠性的 Node.js 应用,有效应对各类运行时错误。
相关文章
文件系统
Node.js 的 fs 模块提供了与文件系统交互的 API,几乎涵盖了所有标准文件操作。它支持三种风格的 API:同步、回调式异步 和 Promise 式异步。下面详细介绍这些 API 的选择策略、流式读写、文件监视以及常用操作。
异步编程与模式
Node.js 的核心优势在于异步非阻塞 I/O,但这也带来了回调地狱、错误处理复杂等问题。下面从解救方案、工具函数、并发控制、异步迭代以及常见反模式等角度展开。
缓冲区
Buffer 是 Node.js 全局对象,用于处理二进制数据流(如文件、网络数据)。在 ES6 引入 TypedArray 之前,Buffer 是 Node.js 处理二进制的主要方式;现在 Buffer 实现了 Uint8Arra…
全局对象与变量
Node.js 提供模块内可直接使用的全局对象;与浏览器 window 不同,模块顶层的 const/let 不会挂到 global。见 后端入门概览、模块系统。
事件循环
Node.js 的事件循环是其实现非阻塞 I/O 的核心机制。它基于 libuv 库,将各种异步操作(定时器、I/O、setImmediate 等)组织成不同的阶段,每个阶段都有一个先进先出的回调队列。事件循环会按照固定的顺序反复执行…
路径处理
path 模块提供了用于处理和转换文件路径的实用工具。它是 Node.js 核心模块,无需安装即可直接使用。由于不同操作系统(Windows、Linux、macOS)的路径分隔符不同(Windows 使用反斜杠 \,POSIX 使用正…
Series
nodejs
3 / 13