FORMA

错误处理与健壮性

Node.js 错误处理与健壮性

构建健壮的 Node.js 应用,必须系统性地处理各种错误,包括同步、异步、未捕获异常等,并结合进程管理和健康检查保证服务可用性。

一、错误类型

Node.js 内置了多种错误构造函数,所有错误都继承自 Error

类型描述常见场景
Error通用错误主动抛出的未知错误
TypeError参数类型不符合预期调用非函数、读取 null 属性
RangeError数值超出有效范围数组长度负值、递归栈溢出
SyntaxError语法错误(通常发生在启动解析阶段)代码书写错误(eval 也可能触发)
ReferenceError引用未声明变量访问未定义的变量
URIError全局 URI 处理函数错误decodeURI 传入非法字符串

自定义 AppError:为业务错误统一结构,便于分类处理。

js
class AppError extends Error {
    constructor(message, statusCode, isOperational = true) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = isOperational; // 区分可恢复错误 vs 程序缺陷
        Error.captureStackTrace(this, this.constructor);
    }
}

使用示例:

js
throw new AppError("用户不存在", 404);

二、同步与异步错误捕获

1. try/catch 仅适用于同步代码

js
try {
    JSON.parse("invalid json");
} catch (err) {
    console.error("同步错误已捕获", err.message);
}

对于异步回调中的错误,try/catch 无法捕获,因为回调执行时 try 块已结束。

2. 异步错误捕获方法

Promise 链:必须添加 .catch()

js
doSomethingAsync()
    .then(result => process(result))
    .catch(err => console.error("Promise 错误:", err));

async/await:使用 try/catch 包裹 await

js
async function run() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (err) {
        console.error("async 错误:", err);
    }
}

回调风格:错误优先回调的第一个参数为错误对象。

js
fs.readFile("file.txt", (err, data) => {
    if (err) {
        console.error("读取失败", err);
        return;
    }
    // 处理 data
});

三、全局未捕获异常

1. process.on('uncaughtException')

该事件用于捕获未在 try/catch.catch 中处理的同步/异步错误(例如 setTimeout 里的同步错误、未捕获的异常)。它只是最后防线,不应依赖它来维持应用正常运行,因为出错后应用可能处于不一致状态。

js
process.on("uncaughtException", err => {
    console.error("未捕获异常:", err);
    // 执行清理逻辑(关闭数据库连接、服务器等)
    // 然后优雅退出
    process.exit(1);
});

最佳实践:记录错误后立即退出进程(process.exit(1)),并让进程管理器(如 PM2)负责重启。

2. process.on('unhandledRejection')

用于捕获未处理的 Promise 拒绝(没有 .catchtry/catchawait)。

js
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未捕获的错误(通用错误)
2Bash 内置命令使用错误(很少用于 Node)
3内部 JavaScript 解析错误(启动阶段)
4内部执行失败(如 EvalError
5致命错误(V8 内部断言失败)
6非函数异常处理(事件循环中抛出的异常)
7异常处理时再抛出异常(很少见)
9无效参数
12无效调试参数
128 + 信号码被信号终止(如 SIGKILL = 128+9=137)

设置方式:

js
// 方法1:设置 exitCode 并让进程自然退出
process.exitCode = 1;

// 方法2:立即退出(注意会中断尚未完成的 I/O)
process.exit(1);

推荐使用 process.exitCode 并让事件循环清空,避免强制退出导致数据丢失。

五、健康检查与自动重启

生产环境中应使用进程管理器或系统守护工具,实现崩溃自动重启和健康检查。

1. PM2(常用 Node.js 进程管理器)

bash
# 启动应用
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 中配置健康检查:

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

ini
[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

启用并启动:

bash
systemctl enable myapp
systemctl start myapp

3. 健康检查 API 实现

应用本身应提供一个 HTTP 端点(如 /health),返回 200 表示正常。

js
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 标志,与非操作错误(程序缺陷)区分。
  • 注册全局处理器uncaughtExceptionunhandledRejection 用于记录日志并优雅退出,而非维持运行。
  • 合理设置退出码:表示失败原因,便于进程管理器判断是否需要重启。
  • 使用进程管理器:PM2 或 systemd 实现自动重启、日志管理和资源限制。
  • 实现健康检查端点:暴露 /health/ready,让外部系统了解服务状态。

通过以上措施,可以构建高可靠性的 Node.js 应用,有效应对各类运行时错误。

相关文章