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();

学习目标

  1. 理解错误类型:掌握 Node.js 中的各种错误类型
  2. 错误捕获:学会使用不同方式捕获同步和异步错误
  3. 错误处理策略:掌握有效的错误处理策略
  4. 自定义错误:能够创建和使用自定义错误类
  5. 日志记录:学会使用日志库记录错误和调试信息
  6. 调试技巧:掌握 Node.js 应用的调试技巧
  7. 错误监控:了解如何监控和分析应用中的错误

代码优化建议

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:生产环境错误信息泄露

原因

  • 在生产环境中向用户显示了详细的错误信息

解决方案

  • 根据环境变量控制错误信息的详细程度
  • 在生产环境中只返回通用错误消息,隐藏敏感信息

总结

通过本教程的学习,你应该能够:

  1. 理解 Node.js 中的各种错误类型及其特点
  2. 掌握不同场景下的错误捕获方法,包括同步和异步错误
  3. 实现有效的错误处理策略,包括错误传递、转换和分类
  4. 创建和使用自定义错误类,提供更有意义的错误信息
  5. 使用现代日志库记录错误和调试信息
  6. 掌握 Node.js 应用的调试技巧,快速定位问题
  7. 了解如何在生产环境中安全地处理和监控错误

错误处理和调试是 Node.js 开发中的重要技能,良好的错误处理策略可以提高应用的可靠性和可维护性,而有效的调试技巧可以帮助你快速定位和解决问题。在实际开发中,你应该根据应用的具体需求,选择合适的错误处理和调试策略,不断优化和改进你的代码,以构建更加健壮和可靠的 Node.js 应用。

« 上一篇 Node.js 认证与授权 下一篇 » Node.js 性能优化