统一异常处理机制
统一异常处理机制的必要性
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)
})
})
生产环境实践
生产环境中需要考虑:
- 不暴露堆栈信息给客户端
- 友好的错误消息
- 错误分类监控
- 自动告警机制
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)
与前端协作的约定
定义前后端错误交互规范:
-
错误代码分类:
- AUTH_*: 认证相关错误
- VALIDATION_*: 验证错误
- DB_*: 数据库错误
- API_*: 第三方API错误
-
错误消息:
- 开发环境:详细错误
- 生产环境:友好提示
-
错误扩展:
- 提供文档链接
- 给出解决建议
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
上一篇:DTO 与数据格式转换
下一篇:日志系统的集成与配置