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) |
SyntaxError | JSON.parse 无效 JSON | JSON.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 捕获。
⚠️ 永远不要在错误处理中抛出新错误:这会导致错误丢失。
业务场景
- API 错误响应:统一的错误格式,方便前端处理
- 数据库连接失败:重试 + 告警 + 优雅降级
- 第三方 API 超时:超时控制 + 重试 + 熔断
- 内存溢出:监控内存使用,接近阈值时重启
扩展阅读
- Node.js 错误处理最佳实践
- Express 错误处理
- Sentry — 错误监控平台
上一章:第 18 章 · 日志 下一章:第 20 章 · 安全 — CORS、CSRF、XSS、速率限制和 Helmet。