错误处理与调试
What — 是什么
错误处理与调试是 Node.js 生产级应用的核心能力,涵盖错误分类、异步错误捕获、全局错误兜底、调试工具和性能分析,确保应用在异常情况下优雅降级而非崩溃。
核心概念:
- 错误类型:
Error(通用)、TypeError(类型错误)、RangeError(范围错误)、SyntaxError(语法错误)、ReferenceError(引用错误)、SystemError(系统调用错误) - 异步错误捕获:回调中的错误无法被 try/catch 捕获,需用
async/await+try/catch或.catch() - 堆栈跟踪:
Error.stack包含调用链信息,Error.captureStackTrace()可自定义 - debugger:Node.js 内置调试器,
node inspect app.js或通过 Chrome DevTools Protocol - 性能分析:
perf_hooks模块监控事件循环延迟、HTTP 延迟、GC 情况 - heapdump:堆内存快照,用于定位内存泄漏
关键特性:
process.on('uncaughtException')捕获未处理的同步异常(最后防线,之后应退出)process.on('unhandledRejection')捕获未处理的 Promise 拒绝error.code是 Node.js 系统错误的字符串标识(如ENOENT/ECONNREFUSED)V8的--stack-trace-limit控制堆栈深度(默认 10)node --inspect启用远程调试协议
Why — 为什么
适用场景:
- 生产环境错误兜底:防止未捕获异常导致进程崩溃
- 异步代码调试:追踪 Promise 链和 async/await 中的错误来源
- 内存泄漏排查:通过 heapdump 定位持续增长的对象
- 性能瓶颈分析:监控事件循环延迟和 CPU 占用
对比错误处理方式:
| 维度 | try/catch | .catch() | uncaughtException | 错误中间件 |
|---|---|---|---|---|
| 同步错误 | 支持 | 不支持 | 兜底 | 支持 |
| 异步回调 | 不支持 | 不支持 | 兜底 | 支持 |
| Promise | 不支持 | 支持 | 兜底 | 支持 |
| async/await | 支持 | 不支持 | 兜底 | 支持 |
| 适用层级 | 函数级 | Promise 链级 | 进程级 | 框架级 |
优缺点:
- ✅ 优点:
async/await+try/catch是最直觉的错误处理方式- 全局兜底机制防止进程静默崩溃
- Chrome DevTools 远程调试体验好
- heapdump 可离线分析内存问题
- ❌ 缺点:
- 异步回调中的错误容易被遗漏
uncaughtException后进程状态不可靠- 多层 try/catch 使代码冗余
- 生产环境调试工具不如开发环境丰富
How — 怎么用
快速上手
// async/await 错误处理
async function fetchUser(id) {
try {
const user = await db.users.findById(id);
if (!user) throw new NotFoundError('User not found');
return user;
} catch (err) {
if (err instanceof NotFoundError) {
throw err; // 重新抛出业务错误
}
throw new AppError('Database error', { cause: err });
}
}
// 全局兜底
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
process.exit(1); // 必须退出,进程状态不可靠
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
process.exit(1);
});
代码示例
自定义错误类 + 错误中间件:
// 自定义错误类
class AppError extends Error {
constructor(message, statusCode = 500, options = {}) {
super(message, options);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.isOperational = options.isOperational ?? true; // 可预期的业务错误
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, 404, { isOperational: true });
}
}
class ValidationError extends AppError {
constructor(details) {
super('Validation failed', 400, { isOperational: true });
this.details = details;
}
}
class AuthError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401, { isOperational: true });
}
}
// Express 错误中间件(必须 4 个参数)
app.use((err, req, res, next) => {
// 已知的业务错误
if (err.isOperational) {
return res.status(err.statusCode).json({
error: {
code: err.name,
message: err.message,
...(err.details && { details: err.details })
}
});
}
// 未知错误:记录日志,返回通用 500
logger.error('Unexpected error', { err, req });
res.status(500).json({
error: { code: 'INTERNAL_ERROR', message: 'Internal server error' }
});
});
// 包装 async 路由处理器
function asyncHandler(fn) {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await fetchUser(req.params.id);
res.json(user);
}));
调试与性能分析:
const { monitorEventLoopDelay, performance } = require('perf_hooks');
// 监控事件循环延迟
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
setInterval(() => {
console.log('事件循环延迟:', {
min: histogram.min,
max: histogram.max,
mean: histogram.mean,
p99: histogram.percentile(99),
p999: histogram.percentile(99.9)
});
histogram.reset();
}, 60000);
// HTTP 请求计时
const httpTimer = performance.timerify(async function handleRequest(req, res) {
await processRequest(req, res);
});
// 性能观察器
const obs = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
}
});
obs.observe({ type: 'function', buffered: true });
// heapdump 生成(需安装 heapdump 包或使用 v8 模块)
const v8 = require('v8');
function takeHeapSnapshot() {
const stream = v8.getHeapSnapshot();
const fileName = `heap-${Date.now()}.heapsnapshot`;
const fs = require('fs');
stream.pipe(fs.createWriteStream(fileName));
console.log(`Heap snapshot: ${fileName}`);
}
// 信号触发 heapdump
process.on('SIGUSR2', takeHeapSnapshot);
// 内存泄漏检测
setInterval(() => {
const usage = process.memoryUsage();
if (usage.heapUsed > 500 * 1024 * 1024) { // 超过 500MB
console.warn('内存使用过高:', usage);
takeHeapSnapshot();
}
}, 30000);
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| try/catch 捕获不到异步错误 | setTimeout/Promise 回调不在 try 块内 | 用 async/await,或 .catch() 处理 Promise |
| uncaughtException 后继续运行 | 进程状态可能已损坏(文件句柄泄漏等) | 记录日志后 process.exit(1),由 PM2 重启 |
| 错误堆栈不完整 | --stack-trace-limit 默认 10 层 | 启动参数 --stack-trace-limit=50 |
| 系统错误缺少有用信息 | SystemError 的 code/message 不够明确 | 检查 err.code(如 ENOENT)和 err.path |
| heapdump 文件太大 | 堆内存本身过大 | 用 Chrome DevTools 对比两个快照找增长 |
| 调试时断点不生效 | 代码被转译/打包 | 确保 source map 正确,用 --enable-source-maps |
最佳实践
- 所有 async 函数用 try/catch 包裹或用
.catch()处理 - 自定义错误类区分业务错误(isOperational)和系统错误
- Express 使用
asyncHandler包装,避免遗漏 async 错误 uncaughtException只做日志 + 退出,不做业务恢复- 生产环境监控
unhandledRejection,Node.js 15+ 默认导致进程退出 - 使用
--enable-source-maps让编译后代码的错误堆栈指向源码 - 内存超阈值自动生成 heapdump 供离线分析
面试题
Q1: Node.js 中有哪些错误类型?
6 种内置错误类型:①
Error——通用错误基类;②TypeError——值不是预期类型(如undefined.foo);③RangeError——值超出范围(如栈溢出、数组越界);④SyntaxError——语法错误(通常在解析阶段);⑤ReferenceError——引用未声明的变量;⑥SystemError——Node.js 系统调用失败(如文件不存在ENOENT、连接拒绝ECONNREFUSED),有额外的code/errno/syscall/path属性。
Q2: 为什么 try/catch 捕获不到异步回调中的错误?
try/catch 只捕获同一执行上下文(调用栈帧)中的异常。异步回调(setTimeout/Promise.then)在新的调用栈帧中执行,此时 try 块已经退出。例如
try { setTimeout(() => throw new Error(), 0) } catch(e) {}永远捕获不到,因为 throw 发生在 setTimeout 的回调栈中。解决方案:① 使用 async/await——await暂停当前函数,错误在同一个 try 块中被捕获;② 对 Promise 链使用.catch();③ 监听unhandledRejection兜底。
Q3: uncaughtException 和 unhandledRejection 有什么区别?处理原则是什么?
uncaughtException:同步代码中未被 try/catch 捕获的异常冒泡到事件循环时触发。unhandledRejection:Promise 被 reject 但没有.catch()或try/catch处理时触发。处理原则:两者都是最后防线,触发后进程状态不可靠(可能有未完成的 I/O、文件句柄泄漏),应记录错误日志后process.exit(1),由进程管理器(PM2/Systemd)重启。不建议在回调中继续处理请求。Node.js 15+ 中unhandledRejection默认导致进程退出。
Q4: 如何在 Express 中统一处理异步路由错误?
两种方式:①
asyncHandler包装函数——const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next),每个 async 路由用app.get('/path', asyncHandler(async (req, res) => {...}))包装;② Express 5.0 原生支持——路由处理器返回 rejected Promise 时自动调用next(err)。无论哪种方式,错误最终由 4 参数错误中间件统一处理。
Q5: 如何调试 Node.js 应用?有哪些工具?
调试方式:①
node inspect app.js——内置 CLI 调试器,支持断点/单步/变量查看;②node --inspect app.js——启用 Chrome DevTools Protocol,在chrome://inspect中打开 DevTools 调试,支持断点/调用栈/内存分析;③ VS Code 调试——配置launch.json,断点调试体验最好;④console.log+debug包——简单有效,debug库按命名空间开关日志;⑤ndb——Node.js 调试增强工具(已归档);⑥node --prof+--prof-process——CPU 性能分析。
Q6: 如何排查 Node.js 内存泄漏?
排查步骤:① 监控
process.memoryUsage(),确认 heapUsed 持续增长;② 使用v8.getHeapSnapshot()生成堆快照;③ 在不同时间点生成两个快照;④ 用 Chrome DevTools 的 Memory 面板加载两个快照,选择 “Comparison” 视图;⑤ 找到增长的构造函数和保留路径(Retainers);⑥ 根据保留路径定位代码中持有引用的对象。常见泄漏源:全局变量、闭包引用、未移除的事件监听器、未关闭的数据库连接、缓存无限增长。
Q7: Error.captureStackTrace 的作用是什么?
Error.captureStackTrace(targetObject, constructorOpt)在目标对象上创建.stack属性,记录当前调用栈。constructorOpt参数指定截断点——该函数及其以上的调用帧不会出现在堆栈中。自定义错误类中使用:Error.captureStackTrace(this, MyError),这样堆栈从MyError的调用者开始,不包含MyError构造函数本身,使堆栈更清晰。Node.js 内置错误类默认调用此方法。
Q8: 生产环境错误处理最佳实践?
① 分层处理:函数级 try/catch → 框架级错误中间件 → 进程级 uncaughtException/unhandledRejection;② 区分错误类型:业务错误(可操作,返回 4xx)vs 系统错误(不可操作,返回 5xx);③ 错误日志包含上下文(请求ID/用户ID/参数),用结构化日志(pino/winston);④ 不向客户端暴露内部错误详情(堆栈/SQL/文件路径);⑤ 设置
--enable-source-maps让编译后代码的错误堆栈指向 TypeScript 源码;⑥ 监控错误率和错误类型,设置告警阈值。
相关链接:
- 事件循环
- 异步编程
- 日志与监控
- Node.js Errors 文档:https://nodejs.org/api/errors.html