阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 错误处理策略

错误处理策略

作者:陈川 阅读数:49930人阅读 分类: Node.js

错误处理策略

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

典型错误处理模式包括:

  1. 优先检查 err 参数
  2. 使用 return 终止后续逻辑执行
  3. 错误信息应包含足够上下文(如文件路径)

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌