Node.js 错误处理与调试
核心知识点
错误类型
Node.js 中的错误类型:
- Error:所有错误的基类
- SyntaxError:语法错误
- ReferenceError:引用错误(访问不存在的变量)
- TypeError:类型错误(操作类型不正确)
- RangeError:范围错误(值超出有效范围)
- URIError:URI 错误(URI 相关操作失败)
- CustomError:自定义错误
错误捕获
错误捕获的方式:
- try/catch:捕获同步错误
- 回调函数:通过回调函数的第一个参数传递错误
- **Promise.catch()**:捕获 Promise 错误
- async/await + try/catch:捕获异步错误
错误处理策略
有效的错误处理策略:
- 错误传递:将错误传递给调用者处理
- 错误转换:将底层错误转换为更有意义的错误
- 错误分类:根据错误类型采取不同的处理策略
- 错误恢复:在可能的情况下从错误中恢复
- 错误记录:记录错误信息以便调试
日志记录
日志记录的最佳实践:
- 日志级别:使用不同级别的日志(debug、info、warn、error)
- 结构化日志:使用 JSON 等结构化格式记录日志
- 上下文信息:在日志中包含足够的上下文信息
- 日志轮转:定期轮转日志文件,避免文件过大
- 日志聚合:使用日志聚合工具集中管理日志
常用的日志库:
- winston:功能丰富的日志库
- bunyan:结构化日志库
- pino:高性能日志库
- morgan:HTTP 请求日志中间件
调试工具
Node.js 调试工具:
- Node.js 内置调试器:使用
node --inspect启动调试 - Chrome DevTools:通过 Chrome 调试 Node.js 应用
- Visual Studio Code:内置调试支持
- ndb:改进的 Node.js 调试体验
- debug:轻量级调试工具
实用案例
案例一:同步错误处理
// 同步错误处理
function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
}
function processNumbers(numbers) {
if (!Array.isArray(numbers)) {
throw new TypeError('参数必须是数组');
}
if (numbers.length === 0) {
throw new RangeError('数组不能为空');
}
return numbers.map(num => {
if (typeof num !== 'number') {
throw new TypeError(`元素 ${num} 不是数字`);
}
return num * 2;
});
}
// 使用 try/catch 捕获错误
function safeProcess() {
try {
const result1 = divide(10, 2);
console.log('除法结果:', result1);
const result2 = processNumbers([1, 2, 3, 4, 5]);
console.log('处理后的数组:', result2);
// 故意触发错误
const result3 = divide(10, 0);
console.log('这个不会执行');
} catch (error) {
console.error('捕获到错误:', error.message);
console.error('错误类型:', error.constructor.name);
console.error('错误堆栈:', error.stack);
}
}
safeProcess();案例二:回调错误处理
const fs = require('fs');
// 回调风格的错误处理
function readFileCallback(filePath, callback) {
fs.readFile(filePath, 'utf8', (error, data) => {
if (error) {
// 错误处理
console.error('读取文件错误:', error);
return callback(error);
}
// 处理数据
console.log('文件内容长度:', data.length);
callback(null, data);
});
}
function processFileCallback(filePath, callback) {
readFileCallback(filePath, (error, data) => {
if (error) {
return callback(error);
}
// 处理数据
const processedData = data.toUpperCase();
callback(null, processedData);
});
}
// 使用示例
processFileCallback('example.txt', (error, result) => {
if (error) {
console.error('处理文件失败:', error);
} else {
console.log('处理结果:', result);
}
});案例三:Promise 错误处理
const fs = require('fs').promises;
// Promise 风格的错误处理
function readFilePromise(filePath) {
return fs.readFile(filePath, 'utf8')
.then(data => {
console.log('文件内容长度:', data.length);
return data;
})
.catch(error => {
console.error('读取文件错误:', error);
throw error; // 重新抛出错误
});
}
function processFilePromise(filePath) {
return readFilePromise(filePath)
.then(data => {
const processedData = data.toUpperCase();
return processedData;
})
.catch(error => {
console.error('处理文件错误:', error);
throw new Error(`处理文件失败: ${error.message}`);
});
}
// 使用示例
processFilePromise('example.txt')
.then(result => {
console.log('处理结果:', result);
})
.catch(error => {
console.error('最终错误:', error);
});案例四:async/await 错误处理
const fs = require('fs').promises;
// async/await 风格的错误处理
async function readFileAsync(filePath) {
try {
const data = await fs.readFile(filePath, 'utf8');
console.log('文件内容长度:', data.length);
return data;
} catch (error) {
console.error('读取文件错误:', error);
throw error;
}
}
async function processFileAsync(filePath) {
try {
const data = await readFileAsync(filePath);
const processedData = data.toUpperCase();
return processedData;
} catch (error) {
console.error('处理文件错误:', error);
throw new Error(`处理文件失败: ${error.message}`);
}
}
// 使用示例
async function main() {
try {
const result = await processFileAsync('example.txt');
console.log('处理结果:', result);
} catch (error) {
console.error('最终错误:', error);
}
}
main();案例五:自定义错误
// 自定义错误类
class AppError extends Error {
constructor(message, statusCode, options = {}) {
super(message);
// 设置错误名称
this.name = this.constructor.name;
// 设置状态码
this.statusCode = statusCode;
// 设置错误状态
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
// 设置是否为可操作错误
this.isOperational = options.isOperational !== false;
// 设置额外信息
this.extra = options.extra;
// 捕获堆栈跟踪
Error.captureStackTrace(this, this.constructor);
}
}
// 使用自定义错误
function validateUser(user) {
if (!user) {
throw new AppError('用户不存在', 404);
}
if (!user.email) {
throw new AppError('邮箱不能为空', 400, {
extra: { userId: user.id }
});
}
return user;
}
// 错误处理中间件
function errorHandler(error, req, res, next) {
// 设置默认状态码
error.statusCode = error.statusCode || 500;
error.status = error.status || 'error';
// 开发环境显示详细错误
if (process.env.NODE_ENV === 'development') {
res.status(error.statusCode).json({
status: error.status,
error: error,
message: error.message,
stack: error.stack
});
}
// 生产环境隐藏敏感错误
else {
// 只返回可操作错误
if (error.isOperational) {
res.status(error.statusCode).json({
status: error.status,
message: error.message,
extra: error.extra
});
} else {
// 记录未处理的错误
console.error('未处理的错误:', error);
// 返回通用错误消息
res.status(500).json({
status: 'error',
message: '服务器内部错误'
});
}
}
}
// 使用示例
function main() {
try {
const user = { id: 1 };
validateUser(user);
} catch (error) {
console.error('错误名称:', error.name);
console.error('错误消息:', error.message);
console.error('状态码:', error.statusCode);
console.error('状态:', error.status);
console.error('额外信息:', error.extra);
}
}
main();案例六:日志记录
// 1. 安装依赖
// npm install winston
const winston = require('winston');
// 配置日志记录器
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
defaultMeta: { service: 'user-service' },
transports: [
// 错误日志文件
new winston.transports.File({
filename: 'error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// 所有日志文件
new winston.transports.File({
filename: 'combined.log',
maxsize: 5242880, // 5MB
maxFiles: 5
})
]
});
// 开发环境添加控制台输出
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
// 使用日志记录器
function processUser(userId) {
logger.info('开始处理用户', { userId });
try {
if (!userId) {
const error = new Error('用户 ID 不能为空');
logger.error('处理用户失败', { error, userId });
throw error;
}
// 模拟处理
logger.debug('处理用户数据', { userId });
if (userId > 1000) {
logger.warn('用户 ID 超出预期范围', { userId });
}
logger.info('处理用户成功', { userId });
return { id: userId, status: 'processed' };
} catch (error) {
logger.error('处理用户异常', { error: error.message, stack: error.stack, userId });
throw error;
}
}
// 使用示例
function main() {
try {
processUser(123);
processUser(0);
} catch (error) {
console.error('主函数捕获错误:', error);
}
}
main();案例七:调试技巧
// 1. 使用 console.log
function debugWithConsole() {
const x = 10;
console.log('x 的值:', x);
const y = x * 2;
console.log('y 的值:', y);
return y;
}
// 2. 使用 debugger 语句
function debugWithDebugger() {
const x = 10;
debugger; // 断点
const y = x * 2;
debugger; // 断点
return y;
}
// 3. 使用环境变量控制调试
function debugWithEnv() {
if (process.env.DEBUG) {
console.log('调试信息:', { timestamp: new Date() });
}
// 正常逻辑
return 'result';
}
// 4. 使用 debug 模块
// npm install debug
const debug = require('debug')('app:main');
const debugUser = require('debug')('app:user');
function debugWithModule() {
debug('开始执行');
const user = { id: 1, name: '张三' };
debugUser('用户信息:', user);
debug('执行完成');
return user;
}
// 使用示例
console.log('使用 console.log 调试:');
debugWithConsole();
console.log('\n使用环境变量调试:');
process.env.DEBUG = 'true';
debugWithEnv();
console.log('\n使用 debug 模块调试:');
process.env.DEBUG = 'app:*';
debugWithModule();学习目标
- 理解错误类型:掌握 Node.js 中的各种错误类型
- 错误捕获:学会使用不同方式捕获同步和异步错误
- 错误处理策略:掌握有效的错误处理策略
- 自定义错误:能够创建和使用自定义错误类
- 日志记录:学会使用日志库记录错误和调试信息
- 调试技巧:掌握 Node.js 应用的调试技巧
- 错误监控:了解如何监控和分析应用中的错误
代码优化建议
1. 统一错误处理
不好的做法:
function getUser(id) {
if (!id) {
throw new Error('ID 不能为空');
}
return db.query('SELECT * FROM users WHERE id = ?', [id], (error, result) => {
if (error) {
console.error('查询错误:', error);
callback(error);
} else {
callback(null, result[0]);
}
});
}好的做法:
function getUser(id) {
return new Promise((resolve, reject) => {
if (!id) {
return reject(new AppError('ID 不能为空', 400));
}
db.query('SELECT * FROM users WHERE id = ?', [id], (error, result) => {
if (error) {
logger.error('查询用户失败', { error, userId: id });
return reject(new AppError('查询用户失败', 500));
}
if (!result[0]) {
return reject(new AppError('用户不存在', 404));
}
resolve(result[0]);
});
});
}
// 使用
async function getUserHandler(req, res, next) {
try {
const user = await getUser(req.params.id);
res.json(user);
} catch (error) {
next(error);
}
}2. 避免错误吞噬
不好的做法:
function riskyOperation() {
try {
// 可能出错的操作
return dangerousCalculation();
} catch (error) {
// 错误被吞噬
console.log('发生错误');
return null;
}
}好的做法:
function riskyOperation() {
try {
// 可能出错的操作
return dangerousCalculation();
} catch (error) {
// 记录错误
logger.error('危险操作失败', { error });
// 重新抛出错误或返回有意义的值
throw new AppError('操作失败', 500, { originalError: error.message });
}
}3. 结构化日志
不好的做法:
function processOrder(order) {
console.log('处理订单:', order.id);
try {
// 处理逻辑
console.log('订单处理成功:', order.id);
} catch (error) {
console.error('订单处理失败:', order.id, error);
}
}好的做法:
function processOrder(order) {
logger.info('开始处理订单', { orderId: order.id, amount: order.amount });
try {
// 处理逻辑
logger.info('订单处理成功', {
orderId: order.id,
status: 'completed',
timestamp: new Date()
});
} catch (error) {
logger.error('订单处理失败', {
orderId: order.id,
error: error.message,
stack: error.stack
});
throw error;
}
}4. 错误分类处理
不好的做法:
function handleError(error) {
console.error('错误:', error);
res.status(500).send('错误');
}好的做法:
function handleError(error, req, res, next) {
if (error instanceof ValidationError) {
return res.status(400).json({ error: error.message });
}
if (error instanceof AuthenticationError) {
return res.status(401).json({ error: error.message });
}
if (error instanceof AuthorizationError) {
return res.status(403).json({ error: error.message });
}
if (error instanceof NotFoundError) {
return res.status(404).json({ error: error.message });
}
// 其他错误
logger.error('未处理的错误:', error);
res.status(500).json({ error: '服务器内部错误' });
}5. 使用 Promise 链错误处理
不好的做法:
function fetchData() {
return fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
if (!data) {
throw new Error('数据为空');
}
return data;
});
}
// 调用时没有捕获错误
fetchData().then(data => console.log(data));好的做法:
function fetchData() {
return fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP 错误: ${response.status}`);
}
return response.json();
})
.then(data => {
if (!data) {
throw new Error('数据为空');
}
return data;
})
.catch(error => {
logger.error('获取数据失败', { error });
throw new AppError('获取数据失败', 500, { originalError: error.message });
});
}
// 调用时捕获错误
fetchData()
.then(data => console.log(data))
.catch(error => console.error('错误:', error));常见问题与解决方案
问题1:异步错误未被捕获
原因:
- 在异步回调中抛出的错误不会被外层的 try/catch 捕获
解决方案:
- 使用 Promise 或 async/await 处理异步操作
- 在回调函数中正确处理错误并传递给调用者
问题2:错误信息不明确
原因:
- 错误消息过于简单,缺少上下文信息
解决方案:
- 创建自定义错误类,包含详细的错误信息和上下文
- 在错误消息中包含足够的信息以便调试
问题3:日志文件过大
原因:
- 日志记录过多,没有设置日志轮转
解决方案:
- 配置日志轮转,限制单个日志文件大小
- 区分不同级别的日志,只记录必要的信息
问题4:调试困难
原因:
- 代码复杂,缺少足够的调试信息
解决方案:
- 使用 debugger 语句设置断点
- 使用 debug 模块控制调试信息的输出
- 使用 Chrome DevTools 或 VS Code 进行调试
问题5:生产环境错误信息泄露
原因:
- 在生产环境中向用户显示了详细的错误信息
解决方案:
- 根据环境变量控制错误信息的详细程度
- 在生产环境中只返回通用错误消息,隐藏敏感信息
总结
通过本教程的学习,你应该能够:
- 理解 Node.js 中的各种错误类型及其特点
- 掌握不同场景下的错误捕获方法,包括同步和异步错误
- 实现有效的错误处理策略,包括错误传递、转换和分类
- 创建和使用自定义错误类,提供更有意义的错误信息
- 使用现代日志库记录错误和调试信息
- 掌握 Node.js 应用的调试技巧,快速定位问题
- 了解如何在生产环境中安全地处理和监控错误
错误处理和调试是 Node.js 开发中的重要技能,良好的错误处理策略可以提高应用的可靠性和可维护性,而有效的调试技巧可以帮助你快速定位和解决问题。在实际开发中,你应该根据应用的具体需求,选择合适的错误处理和调试策略,不断优化和改进你的代码,以构建更加健壮和可靠的 Node.js 应用。