阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 统一异常处理机制

统一异常处理机制

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

统一异常处理机制的必要性

Koa2应用中,异常处理分散在各个中间件和路由处理函数中,导致代码重复且难以维护。统一异常处理机制能集中管理错误,提供一致的错误响应格式,便于调试和日志记录。未捕获的异常可能导致应用崩溃,统一的错误处理能防止这种情况发生。

Koa2的错误处理中间件

Koa2通过中间件机制实现错误处理。最常用的方式是在应用最顶层添加一个错误处理中间件:

app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = {
      code: err.code || 'INTERNAL_ERROR',
      message: err.message || 'Internal Server Error'
    }
    ctx.app.emit('error', err, ctx)
  }
})

这个中间件会捕获所有下游中间件抛出的异常,并返回结构化的错误响应。ctx.app.emit将错误事件发射出去,便于集中记录日志。

自定义错误类型

创建自定义错误类可以更好地分类和处理不同业务场景的错误:

class BusinessError extends Error {
  constructor(code, message, status = 400) {
    super(message)
    this.code = code
    this.status = status
  }
}

class NotFoundError extends BusinessError {
  constructor(message = 'Resource not found') {
    super('NOT_FOUND', message, 404)
  }
}

// 使用示例
router.get('/users/:id', async (ctx) => {
  const user = await User.findById(ctx.params.id)
  if (!user) {
    throw new NotFoundError('User not found')
  }
  ctx.body = user
})

错误响应标准化

统一的错误响应格式有助于前端处理错误。典型的响应结构包含:

{
  "code": "VALIDATION_ERROR",
  "message": "Invalid email format",
  "details": [
    {
      "field": "email",
      "message": "Must be a valid email address"
    }
  ],
  "timestamp": "2023-05-20T14:30:00Z"
}

实现这种格式的错误处理中间件:

app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = {
      code: err.code || 'INTERNAL_ERROR',
      message: err.message,
      details: err.details,
      timestamp: new Date().toISOString()
    }
    
    if (ctx.status >= 500) {
      ctx.app.emit('error', err, ctx)
    }
  }
})

异步错误的特殊处理

Koa2中异步操作需要使用try/catch或返回Promise,否则错误可能无法被捕获:

// 错误示例 - 不会被捕获
router.get('/timeout', async (ctx) => {
  setTimeout(() => {
    throw new Error('This will crash the process')
  }, 100)
})

// 正确做法 - 使用Promise包装
router.get('/timeout', async (ctx) => {
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        // 业务逻辑
        resolve()
      } catch (err) {
        reject(err)
      }
    }, 100)
  })
})

参数验证错误的统一处理

使用Joi等验证库时,可以统一处理验证错误:

const Joi = require('joi')

const userSchema = Joi.object({
  username: Joi.string().min(3).required(),
  email: Joi.string().email().required()
})

router.post('/users', async (ctx) => {
  const { error, value } = userSchema.validate(ctx.request.body)
  if (error) {
    throw new BusinessError(
      'VALIDATION_ERROR',
      'Invalid user data',
      422,
      error.details
    )
  }
  // 处理有效数据
})

对应的错误处理中间件需要支持details:

app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = {
      code: err.code || 'INTERNAL_ERROR',
      message: err.message,
      details: err.details || undefined,
      stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
    }
  }
})

404处理的最佳实践

对于不存在的路由,Koa2默认不会返回404。需要显式处理:

// 在所有路由之后添加
app.use(async (ctx) => {
  throw new NotFoundError()
})

// 或者更详细的404处理
app.use(async (ctx) => {
  ctx.status = 404
  ctx.body = {
    code: 'NOT_FOUND',
    message: `Route ${ctx.method} ${ctx.path} not found`,
    suggestions: [
      '/api/users',
      '/api/products'
    ]
  }
})

错误日志记录

集中记录错误有助于问题排查:

app.on('error', (err, ctx) => {
  // 生产环境使用专业的日志系统
  console.error(`[${new Date().toISOString()}]`, {
    path: ctx.path,
    method: ctx.method,
    status: ctx.status,
    error: {
      message: err.message,
      stack: err.stack,
      code: err.code
    }
  })
  
  // 可以集成Sentry等错误监控系统
  // Sentry.captureException(err)
})

性能考虑

错误处理中间件应该尽可能轻量,避免阻塞:

app.use(async (ctx, next) => {
  const start = Date.now()
  try {
    await next()
  } catch (err) {
    // 快速处理错误
    ctx.status = err.status || 500
    ctx.body = {
      code: err.code || 'INTERNAL_ERROR',
      message: err.message
    }
    // 耗时操作放到事件处理中
    ctx.app.emit('error', err, ctx)
  } finally {
    const ms = Date.now() - start
    ctx.set('X-Response-Time', `${ms}ms`)
  }
})

测试策略

为错误处理编写测试用例:

const request = require('supertest')
const app = require('../app')

describe('Error Handling', () => {
  it('should return 404 for unknown routes', async () => {
    const res = await request(app)
      .get('/nonexistent')
      .expect(404)
    
    expect(res.body).toHaveProperty('code', 'NOT_FOUND')
  })

  it('should handle validation errors', async () => {
    const res = await request(app)
      .post('/users')
      .send({})
      .expect(422)
    
    expect(res.body).toHaveProperty('code', 'VALIDATION_ERROR')
    expect(res.body.details).toBeInstanceOf(Array)
  })
})

生产环境实践

生产环境中需要考虑:

  1. 不暴露堆栈信息给客户端
  2. 友好的错误消息
  3. 错误分类监控
  4. 自动告警机制
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    
    const isProduction = process.env.NODE_ENV === 'production'
    const isClientError = ctx.status < 500
    
    ctx.body = {
      code: err.code || 'INTERNAL_ERROR',
      message: isClientError || !isProduction 
        ? err.message 
        : 'Something went wrong',
      ...(!isProduction && { stack: err.stack })
    }
    
    if (!isClientError) {
      ctx.app.emit('error', err, ctx)
    }
  }
})

HTTP状态码的最佳实践

合理使用HTTP状态码:

  • 400 Bad Request: 客户端请求错误
  • 401 Unauthorized: 未认证
  • 403 Forbidden: 无权限
  • 404 Not Found: 资源不存在
  • 422 Unprocessable Entity: 验证错误
  • 500 Internal Server Error: 服务器内部错误
// 示例使用
throw new BusinessError('AUTH_FAILED', 'Invalid credentials', 401)
throw new BusinessError('PERMISSION_DENIED', 'Not enough privileges', 403)

与前端协作的约定

定义前后端错误交互规范:

  1. 错误代码分类:

    • AUTH_*: 认证相关错误
    • VALIDATION_*: 验证错误
    • DB_*: 数据库错误
    • API_*: 第三方API错误
  2. 错误消息:

    • 开发环境:详细错误
    • 生产环境:友好提示
  3. 错误扩展:

    • 提供文档链接
    • 给出解决建议
ctx.body = {
  code: 'PAYMENT_FAILED',
  message: 'Payment processing failed',
  documentation: 'https://api.example.com/docs/errors#PAYMENT_FAILED',
  actions: [
    'Check your payment method',
    'Contact support if problem persists'
  ]
}

中间件执行顺序的影响

错误处理中间件的位置很重要:

// 正确的顺序
app.use(errorLogger)        // 最先记录原始错误
app.use(errorResponder)    // 然后格式化响应
app.use(bodyParser())      // 之后是其他中间件
app.use(router.routes())

// 错误的顺序会导致某些错误无法被捕获
app.use(bodyParser())
app.use(router.routes())
app.use(errorResponder)    // 无法捕获bodyParser的错误

第三方中间件的错误处理

处理第三方中间件可能抛出的错误:

const koaBody = require('koa-body')

// 手动包装第三方中间件
app.use(async (ctx, next) => {
  try {
    await koaBody()(ctx, next)
  } catch (err) {
    if (err.status === 413) {
      throw new BusinessError(
        'FILE_TOO_LARGE', 
        'Uploaded file exceeds size limit',
        413
      )
    }
    throw err
  }
})

数据库错误的处理

数据库操作错误的统一转换:

router.get('/users/:id', async (ctx) => {
  try {
    const user = await User.findById(ctx.params.id)
    if (!user) throw new NotFoundError('User not found')
    ctx.body = user
  } catch (err) {
    // 转换Mongoose错误
    if (err.name === 'CastError') {
      throw new BusinessError('INVALID_ID', 'Invalid user ID', 400)
    }
    throw err
  }
})

性能监控集成

将错误与性能监控结合:

app.use(async (ctx, next) => {
  const start = Date.now()
  try {
    await next()
  } catch (err) {
    // 记录错误和性能数据
    monitor.trackError(err, {
      path: ctx.path,
      duration: Date.now() - start
    })
    throw err
  } finally {
    monitor.trackRequest({
      path: ctx.path,
      status: ctx.status,
      duration: Date.now() - start
    })
  }
})

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益,请来信告知我们删除。邮箱:cc@cccx.cn

前端川

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