您现在的位置是:网站首页 > 错误处理机制与调试文章详情

错误处理机制与调试

错误处理机制的基本概念

Express框架中的错误处理机制是构建健壮后端服务的关键部分。错误处理不仅仅是捕获异常,更是一套完整的流程控制策略。在Node.js的异步环境中,错误可能出现在任何环节,包括路由处理、中间件执行、数据库操作等场景。

传统的try-catch块在异步代码中往往失效,比如以下代码就无法捕获setTimeout中的错误:

try {
  setTimeout(() => {
    throw new Error('异步错误');
  }, 100);
} catch (err) {
  console.log('这里捕获不到错误'); // 不会执行
}

Express中的错误处理中间件

Express通过特殊形式的中间件处理错误,这类中间件需要接收四个参数:(err, req, res, next)。当调用next(err)或抛出异常时,Express会跳过所有常规中间件,直接进入错误处理流程。

基本错误处理中间件示例:

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('服务器内部错误!');
});

更完整的实现应该包含错误分类处理:

app.use((err, req, res, next) => {
  if (err instanceof DatabaseError) {
    res.status(503).json({ error: '数据库服务不可用' });
  } else if (err instanceof AuthError) {
    res.status(401).json({ error: '认证失败' });
  } else {
    res.status(err.status || 500).json({
      error: err.message || '未知错误'
    });
  }
});

异步错误的特殊处理

在async/await函数中,错误处理需要特别注意。未捕获的Promise拒绝会导致整个应用崩溃。以下是常见错误模式:

// 危险写法:未处理Promise拒绝
app.get('/user', async (req, res) => {
  const user = await User.findById(req.params.id);
  res.json(user);
});

// 正确写法:使用try-catch
app.get('/user', async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    res.json(user);
  } catch (err) {
    next(err); // 将错误传递给错误处理中间件
  }
});

Express 5.x将自动处理async函数中的拒绝,但在4.x版本中需要手动包装:

const asyncHandler = fn => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/user', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  res.json(user);
}));

自定义错误类型

创建特定错误类型可以提升错误处理的精确度:

class ApiError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

// 使用示例
app.get('/admin', (req, res, next) => {
  if (!req.user.isAdmin) {
    throw new ApiError('权限不足', 403);
  }
  // ...
});

日志记录策略

完善的错误处理必须包含日志记录。Winston或Morgan等库可以创建分级别、分文件的日志系统:

const winston = require('winston');
const { combine, timestamp, printf } = winston.format;

const errorFormat = printf(({ level, message, timestamp, stack }) => {
  return `${timestamp} ${level}: ${message} - ${stack || '无堆栈信息'}`;
});

const logger = winston.createLogger({
  level: 'error',
  format: combine(
    timestamp(),
    errorFormat
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log' })
  ]
});

// 在错误处理中间件中使用
app.use((err, req, res, next) => {
  logger.error({
    message: err.message,
    stack: err.stack
  });
  next(err);
});

开发环境与生产环境的差异

错误响应在不同环境应有不同表现。开发环境需要详细错误信息:

app.use((err, req, res, next) => {
  if (process.env.NODE_ENV === 'development') {
    res.status(err.status || 500).json({
      error: {
        message: err.message,
        stack: err.stack,
        details: err.details
      }
    });
  } else {
    res.status(err.status || 500).json({
      error: {
        message: err.message
      }
    });
  }
});

HTTP状态码的正确使用

常见状态码应用场景:

  • 400 Bad Request:请求参数错误
  • 401 Unauthorized:未认证
  • 403 Forbidden:无权限
  • 404 Not Found:资源不存在
  • 422 Unprocessable Entity:语义错误
  • 500 Internal Server Error:服务器内部错误

示例实现:

app.post('/login', (req, res, next) => {
  if (!req.body.email || !req.body.password) {
    throw new ApiError('邮箱和密码必填', 400);
  }
  
  const user = users.find(u => u.email === req.body.email);
  if (!user) {
    throw new ApiError('用户不存在', 404);
  }
  
  if (user.password !== req.body.password) {
    throw new ApiError('密码错误', 401);
  }
  
  // ...
});

调试技巧与实践

Node.js调试有多种方式:

  1. 使用console.log(最简单但不够专业)
  2. Node.js内置调试器:
    node inspect app.js
    
  3. Chrome DevTools调试:
    node --inspect app.js
    
  4. VS Code调试配置:
    {
      "version": "0.2.0",
      "configurations": [
        {
          "type": "node",
          "request": "launch",
          "name": "启动程序",
          "skipFiles": ["<node_internals>/**"],
          "program": "${workspaceFolder}/app.js"
        }
      ]
    }
    

调试中间件流程示例:

app.use((req, res, next) => {
  console.log('请求到达时间:', new Date());
  console.log('请求方法:', req.method);
  console.log('请求路径:', req.path);
  next();
});

app.get('/debug', (req, res, next) => {
  debugger; // 在这里设置断点
  const result = complexCalculation();
  res.json({ result });
});

请求验证与错误预防

使用Joi等验证库可以在请求进入业务逻辑前拦截错误:

const Joi = require('joi');

const userSchema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),
  email: Joi.string().email({ minDomainSegments: 2 })
});

app.post('/register', (req, res, next) => {
  const { error } = userSchema.validate(req.body);
  if (error) {
    throw new ApiError(error.details[0].message, 400);
  }
  // 继续处理...
});

数据库错误处理

数据库操作需要特殊错误处理策略:

app.get('/products', async (req, res, next) => {
  try {
    const products = await Product.find()
      .skip(req.query.offset)
      .limit(req.query.limit);
    
    if (!products.length) {
      throw new ApiError('未找到产品', 404);
    }
    
    res.json(products);
  } catch (err) {
    if (err.name === 'CastError') {
      next(new ApiError('无效的查询参数', 400));
    } else if (err.name === 'MongoNetworkError') {
      next(new ApiError('数据库连接失败', 503));
    } else {
      next(err);
    }
  }
});

性能监控与错误关联

将错误与性能数据关联可以帮助定位问题根源:

const onHeaders = require('on-headers');

app.use((req, res, next) => {
  const start = process.hrtime();
  
  onHeaders(res, () => {
    const duration = process.hrtime(start);
    const ms = duration[0] * 1e3 + duration[1] * 1e-6;
    req.requestDuration = ms;
  });
  
  next();
});

app.use((err, req, res, next) => {
  if (req.requestDuration > 1000) {
    err.slowRequest = true;
  }
  next(err);
});

客户端错误报告

前端JavaScript错误可以收集到后端:

app.post('/client-error', express.json(), (req, res) => {
  const { message, stack, url, userAgent } = req.body;
  
  ClientError.create({
    message,
    stack,
    url,
    userAgent,
    timestamp: new Date()
  });
  
  res.sendStatus(200);
});

前端对应代码:

window.addEventListener('error', (event) => {
  fetch('/client-error', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      message: event.message,
      stack: event.error.stack,
      url: window.location.href,
      userAgent: navigator.userAgent
    })
  });
});

测试中的错误模拟

单元测试需要模拟各种错误场景:

const request = require('supertest');

describe('错误处理测试', () => {
  it('应处理404错误', async () => {
    const res = await request(app)
      .get('/nonexistent-route')
      .expect(404);
    
    expect(res.body.error).toBe('未找到路由');
  });

  it('应处理数据库错误', async () => {
    jest.spyOn(User, 'findById').mockRejectedValue(new Error('DB故障'));
    
    const res = await request(app)
      .get('/user/123')
      .expect(500);
    
    expect(res.body.error).toMatch(/数据库错误/);
  });
});

进程级别的错误处理

即使有完善的错误处理,未捕获的异常仍可能导致进程退出:

process.on('uncaughtException', (err) => {
  console.error('未捕获异常:', err);
  // 记录错误后优雅退出
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的Promise拒绝:', reason);
  // 可以在这里记录错误并继续运行
});

错误信息的国际化

多语言应用中的错误消息处理:

const locales = {
  en: {
    'auth.failed': 'Authentication failed',
    'user.not_found': 'User not found'
  },
  zh: {
    'auth.failed': '认证失败',
    'user.not_found': '用户不存在'
  }
};

class I18nError extends ApiError {
  constructor(key, lang = 'en', status = 400) {
    const message = locales[lang]?.[key] || key;
    super(message, status);
    this.key = key;
  }
}

// 使用示例
app.get('/profile', (req, res, next) => {
  const lang = req.acceptsLanguages('zh', 'en') || 'en';
  if (!req.user) {
    throw new I18nError('auth.failed', lang, 401);
  }
  // ...
});

安全考虑

错误处理中的安全注意事项:

  1. 不要在生产环境暴露堆栈跟踪
  2. 数据库错误可能包含敏感信息,需要过滤
  3. 错误消息不应透露系统内部细节

安全错误处理示例:

app.use((err, req, res, next) => {
  // 过滤敏感信息
  const safeError = {
    message: err.message,
    statusCode: err.statusCode
  };
  
  if (process.env.NODE_ENV === 'development') {
    safeError.stack = err.stack;
  }
  
  // 记录完整错误
  logger.error(err);
  
  res.status(safeError.statusCode || 500).json({
    error: safeError
  });
});

微服务架构中的错误传播

在微服务环境中,错误可能需要跨服务传递:

app.get('/order/:id', async (req, res, next) => {
  try {
    const order = await OrderService.get(req.params.id);
    const user = await UserService.get(order.userId);
    
    res.json({ ...order, user });
  } catch (err) {
    if (err.isRemoteError) {
      // 来自远程服务的错误
      next(new ApiError(err.remoteMessage, err.remoteStatus));
    } else {
      next(err);
    }
  }
});

错误重试策略

对于暂时性错误,可以实现自动重试机制:

const retry = async (fn, retries = 3, delay = 100) => {
  try {
    return await fn();
  } catch (err) {
    if (retries <= 0 || !err.isRetryable) {
      throw err;
    }
    await new Promise(res => setTimeout(res, delay));
    return retry(fn, retries - 1, delay * 2);
  }
};

app.get('/data', async (req, res, next) => {
  try {
    const data = await retry(() => fetchExternalApi());
    res.json(data);
  } catch (err) {
    next(err);
  }
});

错误分类与监控

将错误分类有助于监控系统报警:

class ErrorCategory {
  static get DATABASE() { return 'database'; }
  static get AUTH() { return 'auth'; }
  static get VALIDATION() { return 'validation'; }
  // ...
}

app.use((err, req, res, next) => {
  if (err instanceof DatabaseError) {
    err.category = ErrorCategory.DATABASE;
  } else if (err instanceof AuthError) {
    err.category = ErrorCategory.AUTH;
  }
  next(err);
});

// 监控系统可以按分类统计错误

前端显示错误的最佳实践

后端错误如何在前端优雅显示:

// 后端返回结构
{
  "error": {
    "message": "操作失败",
    "code": "INVALID_INPUT",
    "details": [
      {
        "field": "email",
        "message": "必须是有效的邮箱地址"
      }
    ]
  }
}

// 前端处理
function displayError(error) {
  if (error.details) {
    error.details.forEach(detail => {
      const field = document.getElementById(detail.field);
      if (field) {
        field.nextElementSibling.textContent = detail.message;
      }
    });
  } else {
    showToast(error.message);
  }
}

错误处理中间件的顺序

中间件顺序对错误处理至关重要:

// 1. 记录请求
app.use(morgan('dev'));

// 2. 解析请求体
app.use(express.json());

// 3. 路由处理
app.use('/api', apiRouter);

// 4. 404处理(放在所有路由之后)
app.use((req, res, next) => {
  next(new ApiError('未找到路由', 404));
});

// 5. 错误处理(放在最后)
app.use((err, req, res, next) => {
  // 处理所有错误
});

自定义错误页面

针对不同状态码返回不同错误页面:

app.use((err, req, res, next) => {
  const status = err.status || 500;
  
  if (req.accepts('html')) {
    res.status(status).render(`errors/${status}`, {
      message: err.message,
      status
    });
  } else {
    res.status(status).json({
      error: {
        message: err.message,
        status
      }
    });
  }
});

对应视图模板示例(views/errors/404.ejs):

<!DOCTYPE html>
<html>
<head>
  <title>404 页面不存在</title>
</head>
<body>
  <h1>404 - <%= message %></h1>
  <p>您访问的页面不存在,请检查URL是否正确</p>
</body>
</html>

性能与错误处理的平衡

过于详细的错误处理可能影响性能,需要权衡:

// 生产环境可能不需要记录每个错误的完整堆栈
app.use((err, req, res, next) => {
  if (process.env.NODE_ENV === 'production') {
    logger.error(err.message, {
      path: req.path,
      method: req.method,
      status: err.status || 500
    });
  } else {
    logger.error(err.stack);
  }
  next(err);
});

错误处理与事务管理

数据库操作中的错误需要正确处理事务:

app.post('/order', async (req, res, next) => {
  const session = await mongoose.startSession();
  session.startTransaction();
  
  try {
    const order = new Order(req.body);
    await order.save({ session });
    
    await User.updateOne(
      { _id: req.user.id },
      { $push: { orders: order._id } },
      { session }
    );
    
    await session.commitTransaction();
    res.status(201).json(order);
  } catch (err) {
    await session.abortTransaction();
    next(err);
  } finally {
    session.endSession();
  }
});

第三方API错误处理

集成第三方服务时的错误处理模式:

const callThirdPartyAPI = async () => {
  try {
    const response = await axios.get('https://api.example.com/data');
    return response.data;
  } catch (err) {
    if (err.response) {
      // 请求已发出,服务器返回状态码非2xx
      throw new ThirdPartyError(
        `第三方API错误: ${err.response.status}`,
        err.response.status
      );
    } else if (err.request) {
      // 请求已发出但无响应
      throw new ThirdPartyError('第三方API无响应', 504);
    } else {
      // 其他错误
      throw new ThirdPartyError('调用第三方API失败', 500);
    }
  }
};

错误处理中间件的单元测试

测试错误处理中间件的示例:

describe('错误处理中间件', () => {
  let mockReq, mockRes, mockNext;

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步