Node.js 错误处理最佳实践:从回调地狱到优雅降级

By | 2026年10月21日

引言

在 Node.js 开发中,错误处理是构建可靠应用的关键,却常被忽视。许多开发者习惯用 try...catch 包裹一切,或忽略回调中的错误参数,导致应用崩溃、资源泄漏或难以调试的 Bug。本文将带你从基础到进阶,系统掌握 Node.js 错误处理的最佳实践,并给出可直接运行的代码示例。

1. 错误处理的基础:回调、Promise 与 async/await

Node.js 的错误处理模式经历了从回调到 Promise 再到 async/await 的演进。理解每种模式的正确处理方式是基础。

1.1 回调模式:错误优先(Error-First)

Node.js 的核心 API 大多遵循错误优先回调约定:回调的第一个参数是错误对象,第二个是结果。


const fs = require('fs');

fs.readFile('/path/to/file', (err, data) => {
  if (err) {
    // 处理错误
    console.error('读取文件失败:', err);
    return;
  }
  console.log(data.toString());
});

注意:永远不要忽略 err 参数,即使你认为不会出错。

1.2 Promise:使用 .catch() 或 try...catch

Promise 链式调用中,务必在链尾添加 .catch():


const fs = require('fs').promises;

fs.readFile('/path/to/file')
  .then(data => console.log(data.toString()))
  .catch(err => console.error('读取文件失败:', err));

1.3 async/await:同步风格的错误处理

async/await 让异步代码看起来像同步,可以用 try...catch 捕获错误:


const fs = require('fs').promises;

async function readFile() {
  try {
    const data = await fs.readFile('/path/to/file');
    console.log(data.toString());
  } catch (err) {
    console.error('读取文件失败:', err);
  }
}

💡 提示:在 async 函数中,未捕获的 Promise 拒绝会导致 unhandledRejection,应始终使用 try…catch 或 .catch()。

2. 创建自定义错误类

内置的 Error 类信息有限。通过继承 Error 创建自定义错误类,可以携带更多上下文,并便于区分错误类型。


class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.isOperational = true; // 标记为可操作错误
    Error.captureStackTrace(this, this.constructor);
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
  }
}

class NotFoundError extends AppError {
  constructor(message) {
    super(message, 404);
  }
}

最佳实践:为可预见的错误(如参数校验失败、资源不存在)创建自定义错误类,并标记 isOperational 为 true。对于程序错误(如 TypeError),则不应标记,以便在全局处理中区分。

3. 集中式错误处理

在 Express 等框架中,集中处理错误可以避免重复代码,并确保一致性。

3.1 Express 错误处理中间件


// 错误处理中间件(4个参数)
app.use((err, req, res, next) => {
  // 记录错误
  console.error(err);

  // 区分可操作错误与程序错误
  if (err.isOperational) {
    res.status(err.statusCode || 500).json({
      status: 'error',
      message: err.message
    });
  } else {
    // 程序错误:不泄露细节,发送通用消息
    res.status(500).json({
      status: 'error',
      message: '服务器内部错误'
    });
  }
});

3.2 使用 async/await 的包装函数

Express 不会自动捕获 async 函数中的错误,需要手动包装:


const asyncHandler = fn => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// 使用
app.get('/user/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    throw new NotFoundError('用户不存在');
  }
  res.json(user);
}));

💡 提示:许多第三方库(如 express-async-errors)可以自动处理,但了解原理很重要。

4. 日志记录与监控

错误处理不仅仅是响应请求,还需要记录和监控。使用 Winston 或 Pino 等日志库,并区分错误级别。


const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.Console({ format: winston.format.simple() })
  ]
});

// 在错误处理中间件中记录
app.use((err, req, res, next) => {
  logger.error({
    message: err.message,
    stack: err.stack,
    url: req.originalUrl,
    method: req.method,
    ip: req.ip
  });
  // ...
});

最佳实践:记录错误时包含请求上下文(URL、方法、IP、用户ID),便于排查。生产环境应将日志发送到集中式日志系统(如 ELK、Sentry)。

5. 优雅降级与进程管理

对于未捕获的异常和未处理的 Promise 拒绝,应记录并优雅关闭进程。


process.on('uncaughtException', (err) => {
  logger.error('未捕获异常:', err);
  // 优雅关闭:关闭服务器、数据库连接等
  server.close(() => {
    process.exit(1);
  });
  // 如果关闭失败,强制退出
  setTimeout(() => process.exit(1), 10000);
});

process.on('unhandledRejection', (reason, promise) => {
  logger.error('未处理的 Promise 拒绝:', reason);
  // 根据情况决定是否退出
});

注意:uncaughtException 后进程状态可能不一致,建议记录后退出,由进程管理器(如 PM2)重启。

6. 常见坑与经验

  • 坑1:在回调中抛出异常不会被捕获。应使用 process.nextTick 或直接传递错误。
  • 坑2:忘记 return 导致后续代码执行。在错误处理中及时 return。
  • 坑3:在生产环境泄露错误堆栈。始终根据环境决定是否返回详细错误信息。
  • 坑4:忽略 unhandledRejection 事件。至少记录日志。

7. 总结

本文介绍了 Node.js 错误处理的核心实践:

  • 根据异步模式选择合适的错误处理方式(回调、Promise、async/await)。
  • 创建自定义错误类,区分可操作错误与程序错误。
  • 使用集中式错误处理中间件,统一响应格式。
  • 记录详细日志并监控错误。
  • 处理未捕获异常,优雅降级。

下一步,你可以探索使用 Sentry 进行错误追踪,或使用 OpenTelemetry 实现分布式追踪。希望这些实践能帮助你构建更健壮的 Node.js 应用。

延伸阅读