阿里云主机折上折
  • 微信号
Current Site:Index > Unified exception handling mechanism

Unified exception handling mechanism

Author:Chuan Chen 阅读数:54898人阅读 分类: Node.js

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:

  1. Not exposing stack traces to clients
  2. User-friendly error messages
  3. Error classification and monitoring
  4. 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:

  1. Error code categories:

    • AUTH_*: Authentication-related errors
    • VALIDATION_*: Validation errors
    • DB_*: Database errors
    • API_*: Third-party API errors
  2. Error messages:

    • Development environment: Detailed errors
    • Production environment: Friendly prompts
  3. 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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.