错误处理策略
错误处理策略
Node.js 作为异步事件驱动的运行时,错误处理机制与传统同步编程有显著差异。合理的错误处理策略能提升应用稳定性,避免未捕获异常导致进程崩溃。以下是 Node.js 中常见的错误处理模式与实践方案。
回调函数的错误处理
Node.js 早期广泛采用 error-first callback 模式,异步函数通过回调的第一个参数传递错误对象:
const fs = require('fs');
fs.readFile('/nonexistent/file.txt', (err, data) => {
if (err) {
console.error('读取文件失败:', err.message);
return;
}
console.log('文件内容:', data.toString());
});
典型错误处理模式包括:
- 优先检查 err 参数
- 使用 return 终止后续逻辑执行
- 错误信息应包含足够上下文(如文件路径)
Promise 的错误处理
现代 Node.js 推荐使用 Promise 链式调用,通过 .catch()
捕获异常:
const fs = require('fs').promises;
fs.readFile('/nonexistent/file.txt')
.then(data => console.log('文件内容:', data.toString()))
.catch(err => {
console.error('操作失败:', err.stack);
// 可返回兜底数据
return Buffer.from('默认内容');
});
关键实践:
- 每个 Promise 链必须包含 catch 处理
- 错误冒泡可通过重新抛出实现:
throw new Error('包装错误')
- 使用
Promise.all
时注意部分失败场景
async/await 的 try-catch
使用 async 函数时,try-catch 是最直观的错误处理方式:
async function processFile() {
try {
const data = await fs.readFile('/nonexistent/file.txt');
const processed = await transformData(data);
return processed;
} catch (err) {
if (err.code === 'ENOENT') {
// 特定错误类型处理
await createDefaultFile();
} else {
// 其他错误向上抛出
throw err;
}
}
}
注意事项:
- 避免过度嵌套 try-catch 块
- 区分可恢复错误与致命错误
- 异步错误不会冒泡到外层同步代码
事件发射器的错误处理
EventEmitter 需监听 error 事件,否则会抛出未捕获异常:
const { EventEmitter } = require('events');
class MyEmitter extends EventEmitter {}
const emitter = new MyEmitter();
emitter.on('error', (err) => {
console.error('发射器错误:', err.message);
});
emitter.emit('error', new Error('示例错误'));
最佳实践:
- 重要事件发射器必须绑定 error 监听器
- 错误事件应包含发生时的状态信息
- 考虑使用 domain 模块处理复杂事件流(已弃用但仍有参考价值)
进程级错误处理
全局错误捕获可防止进程意外退出:
process.on('uncaughtException', (err) => {
console.error('未捕获异常:', err);
// 记录日志后主动终止进程
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的拒绝:', reason);
// 可在此处记录 Promise 状态
});
关键点:
- uncaughtException 后应尽快终止进程
- 使用 PM2 等进程管理器实现自动重启
- 区分开发环境(详细日志)与生产环境(优雅降级)
错误分类与自定义错误
创建特定错误类型有助于错误处理:
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = 'DatabaseError';
this.query = query;
this.stack = `${this.stack}\nQuery: ${query}`;
}
}
try {
throw new DatabaseError('连接超时', 'SELECT * FROM users');
} catch (err) {
if (err instanceof DatabaseError) {
console.error('数据库操作失败:', err.query);
}
}
推荐做法:
- 继承 Error 基类创建业务错误
- 附加调试信息(如 SQL、请求参数)
- 使用
err.code
定义机器可读的错误码
日志记录策略
有效的错误日志应包含:
const winston = require('winston');
const logger = winston.createLogger({
level: 'error',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log' })
]
});
function handleError(err) {
logger.error({
message: err.message,
stack: err.stack,
context: {
userId: 123,
apiPath: '/v1/users'
}
});
}
日志要点:
- 结构化日志(JSON 格式)
- 包含调用链信息(requestId)
- 敏感信息脱敏处理
- 区分错误级别(error/warn/info)
HTTP 错误响应
Web 服务应返回标准化的错误响应:
const express = require('express');
const app = express();
app.get('/api/data', async (req, res) => {
try {
const data = await fetchData();
res.json(data);
} catch (err) {
res.status(500).json({
error: 'SERVER_ERROR',
message: '服务端处理失败',
detail: process.env.NODE_ENV === 'development' ? err.stack : undefined
});
}
});
// 404 处理
app.use((req, res) => {
res.status(404).json({
error: 'NOT_FOUND',
message: `路径 ${req.path} 不存在`
});
});
HTTP 错误规范:
- 4xx 表示客户端错误
- 5xx 表示服务端错误
- 错误响应体包含 machine-readable 的错误码
- 生产环境隐藏堆栈信息
重试机制
对于暂时性错误可实现自动重试:
async function withRetry(fn, maxAttempts = 3) {
let attempt = 0;
while (true) {
try {
return await fn();
} catch (err) {
if (!isRetriableError(err) || ++attempt >= maxAttempts) {
throw err;
}
await new Promise(r => setTimeout(r, 1000 * attempt));
}
}
}
function isRetriableError(err) {
return [
'ECONNRESET',
'ETIMEDOUT',
'ENOTFOUND'
].includes(err.code);
}
重试策略要点:
- 指数退避算法避免雪崩
- 设置最大重试次数
- 仅对网络超时等暂时性错误重试
- 记录重试次数用于监控
错误监控与告警
集成 APM 工具实现实时监控:
const Sentry = require('@sentry/node');
Sentry.init({
dsn: 'YOUR_DSN',
tracesSampleRate: 1.0,
attachStacktrace: true
});
// 手动捕获异常
try {
riskyOperation();
} catch (err) {
Sentry.captureException(err, {
tags: { module: 'payment' },
extra: { invoiceId: 12345 }
});
throw err;
}
监控系统功能需求:
- 错误聚合与分类
- 上下文信息收集
- 阈值告警(如每分钟超过 50 次同类型错误)
- 与工单系统集成
测试策略
错误场景应纳入单元测试:
const assert = require('assert');
const { connectDB } = require('./db');
describe('数据库连接', () => {
it('应正确处理认证失败', async () => {
await assert.rejects(
() => connectDB('wrong_credential'),
{
name: 'DatabaseError',
code: 'EAUTHFAIL'
}
);
});
it('应处理连接超时', async function() {
this.timeout(5000); // 设置测试超时
await assert.rejects(
() => connectDB({ host: '1.2.3.4', timeout: 100 }),
/ETIMEDOUT/
);
});
});
测试要点:
- 模拟网络故障(使用 nock 等工具)
- 验证错误类型和错误码
- 包含恢复逻辑的测试
- 压力测试下的错误处理
防御性编程
预防错误发生的编码模式:
function parseJSONSafely(input) {
if (typeof input !== 'string') {
throw new TypeError('输入必须是字符串');
}
try {
return JSON.parse(input);
} catch {
// 返回 null 而不是抛出异常
return null;
}
}
// 参数校验
function createUser(userData) {
const required = ['name', 'email'];
const missing = required.filter(field => !userData[field]);
if (missing.length) {
throw new ValidationError(`缺少必填字段: ${missing.join(', ')}`);
}
// ... 业务逻辑
}
防御性技巧:
- 输入参数类型检查
- 设置默认值
- 使用 TypeScript 静态类型检查
- 关键操作添加前置条件断言
错误处理中间件
Express 的集中式错误处理:
// 错误类定义
class APIError extends Error {
constructor(message, status = 500) {
super(message);
this.status = status;
}
}
// 中间件
function errorHandler(err, req, res, next) {
if (res.headersSent) {
return next(err);
}
const status = err.status || 500;
res.status(status).json({
error: err.name || 'InternalError',
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
}
// 业务路由
app.get('/users/:id', (req, res, next) => {
const user = getUser(req.params.id);
if (!user) {
throw new APIError('用户不存在', 404);
}
res.json(user);
});
// 注册中间件
app.use(errorHandler);
中间件优势:
- 统一错误响应格式
- 避免重复的 try-catch 块
- 支持错误分类处理
- 可集成日志记录
资源清理
确保错误发生时正确释放资源:
async function withTempFile(fn) {
const tempPath = '/tmp/' + Math.random().toString(36).slice(2);
let handle;
try {
handle = await fs.open(tempPath, 'w');
return await fn(handle);
} finally {
if (handle) await handle.close();
try {
await fs.unlink(tempPath);
} catch (cleanupErr) {
console.error('清理临时文件失败:', cleanupErr);
}
}
}
资源管理原则:
- 使用 try-finally 保证清理执行
- 数据库连接使用连接池
- 实现 dispose 模式管理资源生命周期
- 考虑使用 AsyncResource 跟踪异步上下文
错误处理与事务
数据库事务中的错误处理示例:
async function transferFunds(senderId, receiverId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const senderResult = await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2 RETURNING balance',
[amount, senderId]
);
if (senderResult.rows[0].balance < 0) {
throw new InsufficientFundError();
}
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, receiverId]
);
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
事务处理要点:
- 确保 ROLLBACK 在错误时执行
- 注意连接释放放在 finally 块
- 处理死锁等特殊情况
- 考虑使用事务重试装饰器
子进程错误处理
处理 child_process 的异常:
const { spawn } = require('child_process');
const child = spawn('ffmpeg', ['-i', 'input.mp4', 'output.avi']);
child.on('error', (err) => {
console.error('启动子进程失败:', err);
});
child.stderr.on('data', (data) => {
console.error('FFmpeg 错误输出:', data.toString());
});
child.on('exit', (code, signal) => {
if (code !== 0) {
console.error(`子进程异常退出 code=${code} signal=${signal}`);
}
});
子进程注意事项:
- 监听 error 和 exit 事件
- 处理标准错误输出
- 设置超时终止挂起进程
- 使用 execFile 时检查退出码
错误处理与流
Node.js 流的错误处理模式:
const fs = require('fs');
const zlib = require('zlib');
fs.createReadStream('input.txt')
.on('error', err => {
console.error('读取失败:', err.message);
})
.pipe(zlib.createGzip())
.on('error', err => {
console.error('压缩失败:', err.message);
})
.pipe(fs.createWriteStream('output.txt.gz'))
.on('error', err => {
console.error('写入失败:', err.message);
});
流处理关键点:
- 每个流单独监听 error 事件
- 使用 pipeline 替代手动 pipe
- 处理背压导致的错误
- 实现重试逻辑时注意流状态
性能考量
错误处理对性能的影响:
// 低效做法(频繁创建错误对象)
function validateInput(input) {
if (!input) {
throw new Error('输入不能为空');
}
// ...
}
// 优化方案(预定义错误实例)
const EMPTY_INPUT_ERROR = Object.freeze(new Error('输入不能为空'));
function validateInputOptimized(input) {
if (!input) {
throw EMPTY_INPUT_ERROR;
}
// ...
}
// 性能测试
console.time('原始版本');
for (let i = 0; i < 1e6; i++) {
try { validateInput(null); } catch {}
}
console.timeEnd('原始版本');
console.time('优化版本');
for (let i = 0; i < 1e6; i++) {
try { validateInputOptimized(null); } catch {}
}
console.timeEnd('优化版本');
性能优化方向:
- 避免在热路径中创建错误对象
- 减少错误栈生成(Error.captureStackTrace)
- 使用 process.emitWarning 代替非致命错误
- 高频操作考虑返回错误码而非异常
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:async/await语法糖
下一篇:回调地狱问题与解决方案