错误处理 #

一、错误处理概述 #

Fastify提供了完善的错误处理机制,包括自动错误捕获、自定义错误处理器和标准化错误响应。

1.1 错误类型 #

错误类型 说明 状态码
验证错误 Schema验证失败 400
认证错误 未授权访问 401
权限错误 无权限访问 403
资源错误 资源不存在 404
服务器错误 内部错误 500

1.2 错误处理流程 #

text
错误发生
    │
    ▼
┌─────────────────┐
│   onError钩子   │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 错误处理器      │
│ setErrorHandler │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   发送响应      │
└─────────────────┘

二、基本错误处理 #

2.1 抛出错误 #

javascript
fastify.get('/error', async (request, reply) => {
  throw new Error('Something went wrong')
})

响应:

json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Something went wrong"
}

2.2 带状态码的错误 #

javascript
fastify.get('/not-found', async (request, reply) => {
  const error = new Error('User not found')
  error.statusCode = 404
  throw error
})

2.3 使用@fastify/sensible #

javascript
fastify.register(require('@fastify/sensible'))

fastify.get('/bad-request', async (request, reply) => {
  throw fastify.httpErrors.badRequest('Invalid parameters')
})

fastify.get('/unauthorized', async (request, reply) => {
  throw fastify.httpErrors.unauthorized('Token required')
})

fastify.get('/forbidden', async (request, reply) => {
  throw fastify.httpErrors.forbidden('Access denied')
})

fastify.get('/not-found', async (request, reply) => {
  throw fastify.httpErrors.notFound('User not found')
})

fastify.get('/internal-error', async (request, reply) => {
  throw fastify.httpErrors.internalServerError('Database error')
})

三、自定义错误处理器 #

3.1 基本错误处理器 #

javascript
fastify.setErrorHandler((error, request, reply) => {
  reply.code(error.statusCode || 500).send({
    statusCode: error.statusCode || 500,
    error: error.name,
    message: error.message
  })
})

3.2 处理验证错误 #

javascript
fastify.setErrorHandler((error, request, reply) => {
  if (error.validation) {
    reply.code(400).send({
      statusCode: 400,
      error: 'Validation Error',
      message: error.message,
      details: error.validation
    })
    return
  }
  
  reply.send(error)
})

3.3 环境区分 #

javascript
fastify.setErrorHandler((error, request, reply) => {
  const statusCode = error.statusCode || 500
  
  const response = {
    statusCode,
    error: error.name,
    message: statusCode === 500 && process.env.NODE_ENV === 'production'
      ? 'Internal Server Error'
      : error.message
  }
  
  if (process.env.NODE_ENV !== 'production') {
    response.stack = error.stack
  }
  
  reply.code(statusCode).send(response)
})

3.4 完整错误处理器 #

javascript
fastify.setErrorHandler((error, request, reply) => {
  const statusCode = error.statusCode || 500
  
  if (error.validation) {
    reply.code(400).send({
      success: false,
      statusCode: 400,
      error: 'Validation Error',
      message: error.message,
      details: error.validation,
      requestId: request.id
    })
    return
  }
  
  if (statusCode >= 500) {
    fastify.log.error({
      error: error.message,
      stack: error.stack,
      requestId: request.id
    })
  }
  
  reply.code(statusCode).send({
    success: false,
    statusCode,
    error: error.name,
    message: statusCode >= 500 && process.env.NODE_ENV === 'production'
      ? 'Internal Server Error'
      : error.message,
    requestId: request.id
  })
})

四、自定义错误类 #

4.1 基本错误类 #

javascript
class AppError extends Error {
  constructor(message, statusCode = 500) {
    super(message)
    this.name = this.constructor.name
    this.statusCode = statusCode
    this.isOperational = true
    Error.captureStackTrace(this, this.constructor)
  }
}

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

class ValidationError extends AppError {
  constructor(message = 'Validation failed') {
    super(message, 400)
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401)
  }
}

class ForbiddenError extends AppError {
  constructor(message = 'Forbidden') {
    super(message, 403)
  }
}

class ConflictError extends AppError {
  constructor(message = 'Conflict') {
    super(message, 409)
  }
}

module.exports = {
  AppError,
  NotFoundError,
  ValidationError,
  UnauthorizedError,
  ForbiddenError,
  ConflictError
}

4.2 使用自定义错误 #

javascript
const { NotFoundError, ValidationError } = require('./errors')

fastify.get('/users/:id', async (request, reply) => {
  const user = await findUser(request.params.id)
  
  if (!user) {
    throw new NotFoundError('User not found')
  }
  
  return user
})

fastify.post('/users', async (request, reply) => {
  if (!request.body.email) {
    throw new ValidationError('Email is required')
  }
  
  return createUser(request.body)
})

4.3 错误处理器适配 #

javascript
const { AppError } = require('./errors')

fastify.setErrorHandler((error, request, reply) => {
  if (error instanceof AppError) {
    reply.code(error.statusCode).send({
      success: false,
      statusCode: error.statusCode,
      error: error.name,
      message: error.message
    })
    return
  }
  
  if (error.validation) {
    reply.code(400).send({
      success: false,
      statusCode: 400,
      error: 'ValidationError',
      message: error.message,
      details: error.validation
    })
    return
  }
  
  fastify.log.error(error)
  
  reply.code(500).send({
    success: false,
    statusCode: 500,
    error: 'InternalServerError',
    message: process.env.NODE_ENV === 'production'
      ? 'Something went wrong'
      : error.message
  })
})

五、错误钩子 #

5.1 onError钩子 #

javascript
fastify.addHook('onError', async (request, reply, error) => {
  console.error('Error occurred:', error.message)
})

5.2 错误日志 #

javascript
fastify.addHook('onError', async (request, reply, error) => {
  fastify.log.error({
    requestId: request.id,
    method: request.method,
    url: request.url,
    error: {
      name: error.name,
      message: error.message,
      stack: error.stack
    }
  })
})

5.3 错误转换 #

javascript
fastify.addHook('onError', async (request, reply, error) => {
  if (error.name === 'MongoError' && error.code === 11000) {
    error.statusCode = 409
    error.message = 'Duplicate key error'
  }
  
  if (error.name === 'JsonWebTokenError') {
    error.statusCode = 401
    error.message = 'Invalid token'
  }
})

六、路由级错误处理 #

6.1 路由错误处理器 #

javascript
fastify.get('/custom-error', {
  errorHandler: (error, request, reply) => {
    reply.code(400).send({
      custom: true,
      message: error.message
    })
  }
}, async (request, reply) => {
  throw new Error('Custom error handling')
})

6.2 绕过错误处理器 #

javascript
fastify.get('/bypass', async (request, reply) => {
  try {
    await riskyOperation()
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
})

七、验证错误处理 #

7.1 Schema验证错误 #

javascript
fastify.post('/users', {
  schema: {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string' },
        email: { type: 'string', format: 'email' }
      }
    }
  }
}, async (request, reply) => {
  return { created: true }
})

验证失败响应:

json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "body must have required property 'name'"
}

7.2 自定义验证错误消息 #

javascript
fastify.setErrorHandler((error, request, reply) => {
  if (error.validation) {
    const messages = error.validation.map(v => {
      const path = v.instancePath || v.params.missingProperty
      return `${path} ${v.message}`
    })
    
    reply.code(400).send({
      statusCode: 400,
      error: 'Validation Error',
      messages
    })
    return
  }
  
  reply.send(error)
})

八、异步错误处理 #

8.1 未捕获的Promise拒绝 #

javascript
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason)
})

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error)
  process.exit(1)
})

8.2 优雅关闭 #

javascript
const closeListeners = [
  'beforeExit',
  'SIGINT',
  'SIGTERM',
  'SIGUSR1',
  'SIGUSR2'
]

closeListeners.forEach(event => {
  process.on(event, async () => {
    await fastify.close()
    process.exit(0)
  })
})

九、错误响应格式 #

9.1 标准格式 #

javascript
{
  "success": false,
  "statusCode": 400,
  "error": "ValidationError",
  "message": "Email is required",
  "requestId": "req-123",
  "timestamp": "2024-01-01T00:00:00.000Z"
}

9.2 详细格式 #

javascript
{
  "success": false,
  "statusCode": 400,
  "error": "ValidationError",
  "message": "Validation failed",
  "details": [
    {
      "field": "email",
      "message": "must be a valid email"
    },
    {
      "field": "password",
      "message": "must be at least 8 characters"
    }
  ],
  "requestId": "req-123",
  "timestamp": "2024-01-01T00:00:00.000Z"
}

十、最佳实践 #

10.1 错误分类 #

javascript
const ErrorTypes = {
  VALIDATION: 'ValidationError',
  AUTHENTICATION: 'AuthenticationError',
  AUTHORIZATION: 'AuthorizationError',
  NOT_FOUND: 'NotFoundError',
  CONFLICT: 'ConflictError',
  INTERNAL: 'InternalError'
}

class AppError extends Error {
  constructor(type, message, statusCode = 500) {
    super(message)
    this.type = type
    this.statusCode = statusCode
  }
}

10.2 错误工厂 #

javascript
const Errors = {
  notFound: (resource) => new AppError(
    ErrorTypes.NOT_FOUND,
    `${resource} not found`,
    404
  ),
  
  validation: (message) => new AppError(
    ErrorTypes.VALIDATION,
    message,
    400
  ),
  
  unauthorized: (message = 'Unauthorized') => new AppError(
    ErrorTypes.AUTHENTICATION,
    message,
    401
  ),
  
  forbidden: (message = 'Forbidden') => new AppError(
    ErrorTypes.AUTHORIZATION,
    message,
    403
  ),
  
  conflict: (message) => new AppError(
    ErrorTypes.CONFLICT,
    message,
    409
  )
}

fastify.get('/users/:id', async (request, reply) => {
  const user = await findUser(request.params.id)
  
  if (!user) {
    throw Errors.notFound('User')
  }
  
  return user
})

10.3 错误日志级别 #

javascript
fastify.setErrorHandler((error, request, reply) => {
  const statusCode = error.statusCode || 500
  
  if (statusCode >= 500) {
    fastify.log.error(error)
  } else if (statusCode >= 400) {
    fastify.log.warn(error)
  }
  
  reply.code(statusCode).send({
    statusCode,
    error: error.name,
    message: error.message
  })
})

十一、总结 #

本章我们学习了:

  1. 错误类型:验证错误、认证错误、权限错误等
  2. 基本错误处理:抛出错误、状态码错误
  3. 自定义错误处理器:setErrorHandler使用
  4. 自定义错误类:AppError、NotFoundError等
  5. 错误钩子:onError钩子使用
  6. 路由级错误处理:路由错误处理器
  7. 验证错误处理:Schema验证错误
  8. 异步错误处理:未捕获异常处理
  9. 错误响应格式:标准格式、详细格式
  10. 最佳实践:错误分类、错误工厂、日志级别

接下来让我们学习常用中间件!

最后更新:2026-03-28