Unified exception handling mechanism
The Necessity of a Unified Exception Handling Mechanism
In Koa2 applications, exception handling is scattered across various middleware and route handlers, leading to code duplication and maintenance difficulties. A unified exception handling mechanism centralizes error management, provides consistent error response formats, and facilitates debugging and logging. Uncaught exceptions may cause application crashes, and a unified error handling approach can prevent such scenarios.
Error Handling Middleware in Koa2
Koa2 implements error handling through middleware. The most common approach is to add an error-handling middleware at the top level of the application:
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)
}
})
This middleware captures exceptions thrown by downstream middleware and returns structured error responses. ctx.app.emit
emits error events for centralized logging.
Custom Error Types
Creating custom error classes allows better categorization and handling of errors in different business scenarios:
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)
}
}
// Usage example
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
})
Standardizing Error Responses
A unified error response format helps frontend error handling. A typical response structure includes:
{
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
}
],
"timestamp": "2023-05-20T14:30:00Z"
}
Implementing this format in error-handling middleware:
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)
}
}
})
Special Handling for Asynchronous Errors
In Koa2, asynchronous operations require try/catch
or Promise returns; otherwise, errors may not be caught:
// Incorrect example - will not be caught
router.get('/timeout', async (ctx) => {
setTimeout(() => {
throw new Error('This will crash the process')
}, 100)
})
// Correct approach - wrap in a Promise
router.get('/timeout', async (ctx) => {
await new Promise((resolve, reject) => {
setTimeout(() => {
try {
// Business logic
resolve()
} catch (err) {
reject(err)
}
}, 100)
})
})
Unified Handling of Parameter Validation Errors
When using validation libraries like Joi, validation errors can be handled uniformly:
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
)
}
// Process valid data
})
The corresponding error-handling middleware should support 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
}
}
})
Best Practices for 404 Handling
Koa2 does not return 404 by default for non-existent routes. Explicit handling is required:
// Add after all routes
app.use(async (ctx) => {
throw new NotFoundError()
})
// Or more detailed 404 handling
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'
]
}
})
Error Logging
Centralized error logging aids troubleshooting:
app.on('error', (err, ctx) => {
// Use professional logging systems in production
console.error(`[${new Date().toISOString()}]`, {
path: ctx.path,
method: ctx.method,
status: ctx.status,
error: {
message: err.message,
stack: err.stack,
code: err.code
}
})
// Integrate error monitoring systems like Sentry
// Sentry.captureException(err)
})
Performance Considerations
Error-handling middleware should be lightweight to avoid blocking:
app.use(async (ctx, next) => {
const start = Date.now()
try {
await next()
} catch (err) {
// Handle errors quickly
ctx.status = err.status || 500
ctx.body = {
code: err.code || 'INTERNAL_ERROR',
message: err.message
}
// Time-consuming operations go to event handlers
ctx.app.emit('error', err, ctx)
} finally {
const ms = Date.now() - start
ctx.set('X-Response-Time', `${ms}ms`)
}
})
Testing Strategy
Write test cases for error handling:
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)
})
})
Production Environment Practices
In production, consider:
- Not exposing stack traces to clients
- User-friendly error messages
- Error classification and monitoring
- Automated alert mechanisms
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)
}
}
})
Best Practices for HTTP Status Codes
Use HTTP status codes appropriately:
- 400 Bad Request: Client request error
- 401 Unauthorized: Unauthenticated
- 403 Forbidden: Insufficient permissions
- 404 Not Found: Resource does not exist
- 422 Unprocessable Entity: Validation error
- 500 Internal Server Error: Server internal error
// Example usage
throw new BusinessError('AUTH_FAILED', 'Invalid credentials', 401)
throw new BusinessError('PERMISSION_DENIED', 'Not enough privileges', 403)
Collaboration Agreements with Frontend
Define error interaction standards between frontend and backend:
-
Error code categories:
- AUTH_*: Authentication-related errors
- VALIDATION_*: Validation errors
- DB_*: Database errors
- API_*: Third-party API errors
-
Error messages:
- Development environment: Detailed errors
- Production environment: Friendly prompts
-
Error extensions:
- Provide documentation links
- Offer resolution suggestions
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'
]
}
Impact of Middleware Execution Order
The placement of error-handling middleware is crucial:
// Correct order
app.use(errorLogger) // Log raw errors first
app.use(errorResponder) // Then format responses
app.use(bodyParser()) // Followed by other middleware
app.use(router.routes())
// Incorrect order may cause some errors to be missed
app.use(bodyParser())
app.use(router.routes())
app.use(errorResponder) // Cannot catch bodyParser errors
Handling Errors from Third-Party Middleware
Handle errors that may be thrown by third-party middleware:
const koaBody = require('koa-body')
// Manually wrap third-party middleware
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
}
})
Handling Database Errors
Uniformly transform database operation errors:
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) {
// Transform Mongoose errors
if (err.name === 'CastError') {
throw new BusinessError('INVALID_ID', 'Invalid user ID', 400)
}
throw err
}
})
Performance Monitoring Integration
Combine errors with performance monitoring:
app.use(async (ctx, next) => {
const start = Date.now()
try {
await next()
} catch (err) {
// Log errors and performance data
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 与数据格式转换
下一篇:日志系统的集成与配置