强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Node.js 开发指南 / 第 19 章 · 错误处理

第 19 章 · 错误处理

19.1 错误类型

// JavaScript 内置错误类型
const errors = [
  new Error('通用错误'),
  new TypeError('类型错误'),
  new ReferenceError('引用错误'),
  new RangeError('范围错误'),
  new SyntaxError('语法错误'),
  new URIError('URI 错误'),
];

// Node.js 特有错误
const nodeErrors = [
  new EvalError('eval 错误'),
  // SystemError — 系统级错误(文件不存在、网络超时等)
];

错误类型层级

Error
├── TypeError        — 值的类型不对
├── ReferenceError   — 引用不存在的变量
├── RangeError       — 值超出允许范围
├── SyntaxError      — 语法解析失败
├── URIError         — URI 函数参数错误
└── EvalError        — eval 函数错误
错误类型常见触发场景示例
TypeError访问 null 的属性、调用非函数null.foo
ReferenceError访问未声明的变量undeclaredVar
RangeError递归栈溢出、数组长度为负new Array(-1)
SyntaxErrorJSON.parse 无效 JSONJSON.parse('xxx')

19.2 自定义错误类

// 基础自定义错误
class AppError extends Error {
  constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
    super(message);
    this.name = 'AppError';
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = true; // 可预期的错误
    Error.captureStackTrace(this, this.constructor);
  }
}

// 业务错误类
class ValidationError extends AppError {
  constructor(message, details = []) {
    super(message, 400, 'VALIDATION_ERROR');
    this.name = 'ValidationError';
    this.details = details;
  }
}

class NotFoundError extends AppError {
  constructor(resource, id) {
    super(`${resource} 不存在 (ID: ${id})`, 404, 'NOT_FOUND');
    this.name = 'NotFoundError';
  }
}

class UnauthorizedError extends AppError {
  constructor(message = '未授权') {
    super(message, 401, 'UNAUTHORIZED');
    this.name = 'UnauthorizedError';
  }
}

class ForbiddenError extends AppError {
  constructor(message = '权限不足') {
    super(message, 403, 'FORBIDDEN');
    this.name = 'ForbiddenError';
  }
}

class ConflictError extends AppError {
  constructor(message) {
    super(message, 409, 'CONFLICT');
    this.name = 'ConflictError';
  }
}

// 使用
throw new ValidationError('参数验证失败', [
  { field: 'email', message: '邮箱格式不正确' },
  { field: 'name', message: '名称不能为空' },
]);

throw new NotFoundError('用户', 123);

19.3 Express 错误处理

const express = require('express');
const app = express();

// 异步路由包装器
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// 路由示例
app.get('/api/users/:id', asyncHandler(async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) {
    throw new NotFoundError('用户', req.params.id);
  }
  res.json({ data: user });
}));

app.post('/api/users', asyncHandler(async (req, res) => {
  const { valid, errors } = validateUser(req.body);
  if (!valid) {
    throw new ValidationError('参数验证失败', errors);
  }
  const user = await db.users.create(req.body);
  res.status(201).json({ data: user });
}));

// 404 处理(放在所有路由之后)
app.use((req, res) => {
  res.status(404).json({
    error: { code: 'NOT_FOUND', message: `Cannot ${req.method} ${req.url}` },
  });
});

// 全局错误处理中间件(放在最后)
app.use((err, req, res, next) => {
  // 日志记录
  const logger = req.log || console;
  
  if (err.isOperational) {
    // 可预期的业务错误 — warn 级别
    logger.warn({ err, requestId: req.id }, err.message);
  } else {
    // 不可预期的系统错误 — error 级别,需要报警
    logger.error({ err, requestId: req.id }, err.message);
    // 通知监控系统(Sentry 等)
  }

  const statusCode = err.statusCode || 500;
  const response = {
    error: {
      code: err.code || 'INTERNAL_ERROR',
      message: err.isOperational ? err.message : '服务器内部错误',
      ...(err.details && { details: err.details }),
      ...(process.env.NODE_ENV !== 'production' && !err.isOperational && { stack: err.stack }),
    },
  };

  res.status(statusCode).json(response);
});

19.4 全局错误捕获

const logger = require('./logger');

// 未捕获的同步异常
process.on('uncaughtException', (err, origin) => {
  logger.fatal({ err, origin }, '未捕获的异常');
  // 记录日志后优雅退出
  process.exit(1);
});

// 未处理的 Promise 拒绝
process.on('unhandledRejection', (reason, promise) => {
  logger.fatal({ reason, promise }, '未处理的 Promise 拒绝');
  // Node.js 15+ 会导致进程崩溃,建议主动退出
  process.exit(1);
});

// 警告事件
process.on('warning', (warning) => {
  logger.warn({ warning }, 'Node.js 警告');
});

19.5 优雅退出

const server = require('./app').listen(3000);

// 优雅退出函数
async function gracefulShutdown(signal) {
  logger.info({ signal }, '收到关闭信号,开始优雅退出...');

  // 1. 停止接收新连接
  server.close(() => {
    logger.info('HTTP 服务器已关闭');
  });

  // 2. 等待正在处理的请求完成(设置超时)
  const forceExitTimeout = setTimeout(() => {
    logger.error('强制退出(超时)');
    process.exit(1);
  }, 30000); // 30 秒超时

  // 3. 关闭数据库连接
  try {
    await db.disconnect();
    logger.info('数据库连接已关闭');
  } catch (err) {
    logger.error({ err }, '关闭数据库连接失败');
  }

  // 4. 关闭 Redis 连接
  try {
    await redis.quit();
    logger.info('Redis 连接已关闭');
  } catch (err) {
    logger.error({ err }, '关闭 Redis 连接失败');
  }

  // 5. 清理定时器
  clearTimeout(forceExitTimeout);

  logger.info('优雅退出完成');
  process.exit(0);
}

// 监听退出信号
process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // Docker/K8s 发送
process.on('SIGINT', () => gracefulShutdown('SIGINT'));   // Ctrl+C

Docker 中的信号处理

# Dockerfile
# 使用 exec 形式确保 Node.js 进程是 PID 1
CMD ["node", "src/server.js"]
# 而不是
# CMD node src/server.js  # shell 形式,PID 1 是 sh,不传递信号

19.6 错误处理策略总结

错误分类
├── 可预期错误(Operational Error)
│   ├── 用户输入错误 → 400
│   ├── 认证失败 → 401
│   ├── 权限不足 → 403
│   ├── 资源不存在 → 404
│   └── 业务规则冲突 → 409
│
└── 编程错误(Programming Error)
    ├── null 引用 → 500
    ├── 类型错误 → 500
    ├── 逻辑错误 → 500
    └── 第三方库 bug → 500
错误类型处理方式是否报警
可预期错误返回友好错误消息
编程错误记录堆栈 + 通知监控
系统错误(OOM、磁盘满)记录 + 立即重启

注意事项

⚠️ 不要吞掉错误:空的 catch {} 会隐藏问题,至少要记录日志。

⚠️ 区分可预期和不可预期错误:可预期错误返回友好的 HTTP 响应,不可预期错误需要报警。

⚠️ async 函数中的错误:未 await 的 Promise 中的错误不会被 try/catch 捕获。

⚠️ 永远不要在错误处理中抛出新错误:这会导致错误丢失。

业务场景

  1. API 错误响应:统一的错误格式,方便前端处理
  2. 数据库连接失败:重试 + 告警 + 优雅降级
  3. 第三方 API 超时:超时控制 + 重试 + 熔断
  4. 内存溢出:监控内存使用,接近阈值时重启

扩展阅读


上一章第 18 章 · 日志 下一章第 20 章 · 安全 — CORS、CSRF、XSS、速率限制和 Helmet。