Node端异常捕获
'# Node端异常捕获
一、背景与问题
在分布式系统中,Node.js服务常面临复杂的异步调用链。当任意环节发生未处理的异常时,可能导致整个进程崩溃,引发服务不可用。传统做法是使用try/catch包裹所有代码,但这种粗暴的处理方式存在严重缺陷:
- 异步代码无法通过try/catch捕获
- Promise链的拒绝未被处理
- 未捕获的异常可能直接导致进程终止
- 异常信息丢失导致排查困难
在实际项目中,我们曾因未处理的未捕获异常导致生产环境服务中断,损失超过5万元/小时的运维成本。本文将深入探讨Node.js异常捕获的原理、实现方式及工程实践。
二、基本原理
Node.js的异常处理机制基于事件循环和进程级别的异常处理机制,包含两个关键事件:
1. uncaughtException 事件
当未处理的同步异常发生时触发。例如:
throw new Error('Test error');2. unhandledRejection 事件
当未处理的Promise拒绝时触发。例如:
Promise.reject(new Error('Test rejection'));这两个事件的处理方式存在本质区别:
| 事件类型 | 作用域 | 处理方式 | 处理结果 |
|---|---|---|---|
| uncaughtException | 同步代码 | 通过process监听 | 可阻止进程终止 |
| unhandledRejection | Promise链 | 通过process监听 | 可阻止进程终止 |
需要注意:这两个事件的处理函数必须避免引发新的未处理异常,否则可能导致进程重复崩溃。
三、环境准备
创建标准Node.js项目结构:
node-exception-capture/
├── index.js
├── package.json
└── logs/安装依赖:
npm init -y
npm install winston四、核心实现
1. 基础异常捕获(推荐方案)
// index.js
const express = require('express');
const { createLogger, transports, format } = require('winston');
const { combine, timestamp, printf } = format;
const logger = createLogger({
level: 'error',
transports: [
new transports.File({
filename: 'logs/exceptions.log',
format: combine(
timestamp(),
printf(({ level, message, timestamp }) => {
return `${timestamp} [${level}]: ${message}`;
})
)
})
]
});
// 捕获未处理的异常
process.on('uncaughtException', (err, origin) => {
logger.error('Uncaught Exception:', err.message, { origin });
process.exit(1);
});
// 捕获未处理的Promise拒绝
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
// 基础路由
const app = express();
app.get('/', (req, res) => {
throw new Error('Test error');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});关键点解释:
- 使用winston记录异常信息到文件
- 通过process事件监听捕获异常
- 退出码1表示异常退出
- 禁止在处理函数中进行任何可能导致新异常的操作
2. 异步函数异常捕获
async function testAsyncError() {
try {
await Promise.reject('Test rejection');
} catch (err) {
console.error('Caught error:', err);
}
}
testAsyncError();注意:这个示例不会触发unhandledRejection事件,因为Promise被正确处理。
3. 中间件式异常处理(Express场景)
app.use((err, req, res, next) => {
console.error('Error handler:', err.stack);
res.status(500).send('Something broke!');
});需要配合使用:
app.use((req, res, next) => {
try {
next();
} catch (err) {
next(err);
}
});五、完整案例
创建一个模拟的订单处理服务,包含:
- 路由层
- 业务逻辑层
- 数据访问层
- 异常处理层
// db.js
class Database {
async query(sql) {
// 模拟数据库查询
if (Math.random() > 0.5) {
throw new Error('Database error');
}
return { data: 'test' };
}
}
module.exports = new Database();// order.js
const db = require('./db');
async function processOrder(orderId) {
try {
const data = await db.query(`SELECT * FROM orders WHERE id = ${orderId}`);
return data;
} catch (err) {
throw new Error(`Failed to process order ${orderId}: ${err.message}`);
}
}// index.js
const express = require('express');
const { createLogger, transports, format } = require('winston');
const { combine, timestamp, printf } = format;
const { processOrder } = require('./order');
const logger = createLogger({
level: 'error',
transports: [
new transports.File({
filename: 'logs/exceptions.log',
format: combine(
timestamp(),
printf(({ level, message, timestamp }) => {
return `${timestamp} [${level}]: ${message}`;
})
)
})
]
});
process.on('uncaughtException', (err, origin) => {
logger.error('Uncaught Exception:', err.message, { origin });
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
const app = express();
app.use((req, res, next) => {
try {
next();
} catch (err) {
next(err);
}
});
app.use((err, req, res, next) => {
console.error('Error handler:', err.stack);
res.status(500).send('Something broke!');
});
app.get('/orders/:id', async (req, res, next) => {
const { id } = req.params;
try {
const result = await processOrder(id);
res.json(result);
} catch (err) {
next(err);
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});运行这个服务时,故意触发异常:
curl http://localhost:3000/orders/123会看到日志中记录的异常信息,并返回500响应。
六、源码解析
以uncaughtException事件处理为例:
process.on('uncaughtException', (err, origin) => {
logger.error('Uncaught Exception:', err.message, { origin });
process.exit(1);
});origin参数包含异常来源信息(如'eval'或'worker')- 需要确保处理函数不引发新的未处理异常
- 退出码1表示异常退出(0表示正常退出)
- 未处理的异常不会自动恢复进程
七、进阶使用
1. 异常分类处理
process.on('uncaughtException', (err, origin) => {
if (err.message.includes('Database')) {
logger.error('Database error:', err.message, { origin });
} else {
logger.error('Other error:', err.message, { origin });
}
process.exit(1);
});2. 异常重试机制
function retryOnException(maxAttempts = 3) {
return (fn) => {
let attempt = 0;
return (...args) => {
return fn(...args).catch((err) => {
attempt++;
if (attempt < maxAttempts) {
return retryOnException(maxAttempts - attempt)(fn)(...args);
}
throw err;
});
};
};
}3. 异常监控集成
const { Client } = require('@datadog/datadog');
const dogstatsd = new Client({
host: 'localhost',
port: 8125,
service: 'node-exception-capture'
});
process.on('uncaughtException', (err, origin) => {
dogstatsd.increment('uncaught_exception', 1, {
tags: ['origin:' + origin]
});
logger.error('Uncaught Exception:', err.message, { origin });
process.exit(1);
});八、性能与工程实践
1. 性能优化策略
- 避免在每个函数都添加try/catch
- 使用异步错误处理中间件集中处理
- 对关键业务逻辑进行异常埋点
- 使用进程级别的异常处理替代局部处理
2. 异常处理策略
| 场景 | 推荐策略 | 说明 |
|---|---|---|
| 路由处理 | 中间件式处理 | 集中处理异常,避免散落在各处 |
| 业务逻辑 | try/catch + promise链 | 精确控制异常传播路径 |
| 数据访问 | try/catch + catch-all | 避免数据库连接泄漏 |
| 系统级异常 | 进程级处理 | 确保服务稳定性 |
3. 异常监控策略
- 使用winston记录日志
- 集成Prometheus监控异常指标
- 配置Sentry进行错误上报
- 设置异常阈值自动扩容
4. 安全实践
- 避免将敏感信息记录到日志
- 限制异常日志的写入权限
- 对异常信息进行脱敏处理
- 在生产环境禁用详细的堆栈信息
九、常见问题与踩坑
1. 常见错误
错误示例:
setTimeout(() => {
throw new Error('Test error');
}, 1000);问题分析: 这个异常不会触发uncaughtException事件,因为它是异步的。
解决办法: 使用Promise封装或使用try/catch包裹。
2. 错误处理链断裂
错误示例:
app.use((err, req, res, next) => {
console.error(err);
res.status(500).send('Error');
});问题分析: 没有调用next()会导致后续中间件无法执行。
解决办法: 确保错误处理中间件始终调用next()。
3. 未处理的Promise拒绝
错误示例:
Promise.reject(new Error('Test rejection'));问题分析: 会触发unhandledRejection事件,但未处理可能导致进程崩溃。
解决办法: 使用.catch()或try/catch包裹。
4. 异常处理循环
错误示例:
process.on('uncaughtException', (err) => {
throw err;
});问题分析: 会导致进程进入死循环。
解决办法: 确保处理函数不引发新的异常。
十、最佳实践
- 进程级别异常处理:始终监听uncaughtException和unhandledRejection事件
- 中间件式异常处理:在Express等框架中使用错误处理中间件
- 异常分类处理:根据异常类型进行差异化处理
- 异常监控集成:使用Sentry、Datadog等进行异常跟踪
- 日志记录规范:使用winston等工具记录结构化日志
- 异常重试策略:对可重试的异常进行重试处理
- 安全防护:避免敏感信息泄露,限制日志写入权限
十一、总结
Node.js异常捕获是构建稳定系统的关键环节。通过深入理解事件循环机制和异常传播路径,我们可以设计更健壮的异常处理方案。实际开发中应根据场景选择合适的处理方式:
- 同步代码:使用try/catch
- 异步代码:使用async/await + try/catch
- Promise链:使用.catch()或try/catch
- 系统级异常:使用进程级别的事件监听
在实际项目中,建议结合以下实践:
- 使用错误处理中间件集中处理异常
- 配置异常监控系统
- 实现异常分类处理机制
- 遵循安全规范记录日志
- 对关键业务逻辑进行异常埋点
通过合理设计异常处理策略,我们可以显著提升系统的稳定性和可维护性,避免因未处理的异常导致服务中断。记住:异常处理不是简单的捕获和忽略,而是系统设计的重要组成部分。
评论已关闭