Skip to content

全局异常过滤器使用说明

概述

GlobalExceptionsFilter 是 NestJS 应用的全局异常处理过滤器,负责捕获和处理应用中所有的未处理异常,提供统一的错误响应格式。

核心功能

1. 统一异常捕获

  • 捕获所有类型的异常(HttpException 和其他未知异常)
  • 自动识别 HTTP 状态码
  • 提供标准化的错误响应格式

2. 数据库异常处理

  • 特殊处理 MySQL 重复条目错误(ER_DUP_ENTRY)
  • 自动解析并格式化错误消息
  • 提供用户友好的错误提示

3. 标准化响应格式

json
{
  "code": 500,
  "timestamp": "2024-01-01T00:00:00.000Z",
  "path": "/api/users",
  "msg": "错误消息"
}

基础使用

注册全局过滤器

main.ts 中注册全局异常过滤器:

typescript
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { GlobalExceptionsFilter } from './common/filters/GlobalExceptionsFilter'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  
  // 注册全局异常过滤器
  app.useGlobalFilters(new GlobalExceptionsFilter())
  
  await app.listen(3000)
}
bootstrap()

模块级别注册

在特定模块中注册:

typescript
import { Module } from '@nestjs/common'
import { APP_FILTER } from '@nestjs/core'
import { GlobalExceptionsFilter } from './common/filters/GlobalExceptionsFilter'

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionsFilter,
    },
  ],
})
export class AppModule {}

API 参考

Filter 方法

catch(exception, host)

处理捕获的异常

参数:

  • exception: 捕获的异常对象
  • host: ArgumentsHost 对象,用于获取请求上下文

返回值: void

响应格式

typescript
interface ErrorResponse {
  code: number        // HTTP 状态码
  timestamp: string   // ISO 时间戳
  path: string        // 请求路径
  msg: string         // 错误消息
}

完整示例

基础异常处理示例

typescript
// controller.ts
import { Controller, Get, Post } from '@nestjs/common'
import { HttpException, HttpStatus } from '@nestjs/common'

@Controller('users')
export class UsersController {
  @Get(':id')
  findOne(@Param('id') id: string) {
    // 抛出标准 HTTP 异常
    throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    // 抛出自定义异常
    if (!createUserDto.email) {
      throw new HttpException('邮箱不能为空', HttpStatus.BAD_REQUEST)
    }
    
    // 业务逻辑...
  }
}

数据库异常处理示例

typescript
// service.ts
import { Injectable } from '@nestjs/common'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async create(createUserDto: CreateUserDto) {
    try {
      const user = this.usersRepository.create(createUserDto)
      return await this.usersRepository.save(user)
    } catch (error) {
      // 数据库异常会被 GlobalExceptionsFilter 自动处理
      throw error
    }
  }
}

高级用法

自定义异常类型

typescript
// custom-exceptions.ts
import { HttpException, HttpStatus } from '@nestjs/common'

export class BusinessLogicException extends HttpException {
  constructor(message: string, businessCode: number) {
    super(
      {
        message,
        businessCode,
        timestamp: new Date().toISOString(),
      },
      HttpStatus.BAD_REQUEST,
    )
  }
}

// 使用自定义异常
@Post()
create(@Body() createUserDto: CreateUserDto) {
  if (this.isEmailExists(createUserDto.email)) {
    throw new BusinessLogicException('邮箱已存在', 1001)
  }
}

异常日志记录

typescript
// 增强版 GlobalExceptionsFilter
import { Logger } from '@nestjs/common'

@Catch()
export class GlobalExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger(GlobalExceptionsFilter.name)

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()
    
    // 记录异常日志
    this.logger.error(
      `Exception occurred: ${exception.message}`,
      exception.stack,
      `Path: ${request.url}, Method: ${request.method}`
    )

    // 原有处理逻辑...
  }
}

环境相关的错误处理

typescript
@Catch()
export class GlobalExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const isDevelopment = process.env.NODE_ENV === 'development'
    
    // 开发环境显示详细错误信息
    if (isDevelopment) {
      console.error('Detailed error:', exception)
    }

    // 生产环境隐藏敏感信息
    const errorMessage = isDevelopment 
      ? exception.message 
      : '服务器内部错误'

    // 响应处理...
  }
}

最佳实践

1. 异常分类处理

typescript
catch(exception: unknown, host: ArgumentsHost) {
  const ctx = host.switchToHttp()
  const response = ctx.getResponse<Response>()
  const request = ctx.getRequest<Request>()

  let status: number
  let message: string

  if (exception instanceof HttpException) {
    // HTTP 异常处理
    status = exception.getStatus()
    message = exception.message
  } else if (exception.code === 'ER_DUP_ENTRY') {
    // 数据库重复条目异常
    status = HttpStatus.CONFLICT
    const match = exception.message.match(/Duplicate entry '(.+)' for/)
    message = `${match[1]} 已存在`
  } else {
    // 未知异常处理
    status = HttpStatus.INTERNAL_SERVER_ERROR
    message = '服务器内部错误'
    
    // 记录详细错误日志
    console.error('Unhandled exception:', exception)
  }

  response.status(200).json({
    code: status,
    timestamp: new Date().toISOString(),
    path: request.url,
    msg: message,
  })
}

2. 错误码体系

typescript
// error-codes.ts
export enum ErrorCode {
  // 用户相关错误 1000-1999
  USER_NOT_FOUND = 1001,
  USER_ALREADY_EXISTS = 1002,
  INVALID_CREDENTIALS = 1003,
  
  // 业务逻辑错误 2000-2999
  INSUFFICIENT_PERMISSIONS = 2001,
  INVALID_OPERATION = 2002,
  
  // 系统错误 5000-5999
  DATABASE_ERROR = 5001,
  EXTERNAL_SERVICE_ERROR = 5002,
}

// 在异常处理中使用
response.status(200).json({
  code: status,
  errorCode: getErrorCode(exception), // 自定义函数获取错误码
  timestamp: new Date().toISOString(),
  path: request.url,
  msg: message,
})

3. 异常监控集成

typescript
// 集成 Sentry 等监控工具
import * as Sentry from '@sentry/node'

@Catch()
export class GlobalExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    // 发送到监控系统
    Sentry.captureException(exception)
    
    // 原有处理逻辑...
  }
}

注意事项

  1. 响应状态码:虽然内部使用 200 状态码,但 code 字段包含实际的 HTTP 状态码
  2. 数据库异常:目前只处理 MySQL 的 ER_DUP_ENTRY 错误,可根据需要扩展
  3. 日志记录:生产环境中应配置适当的日志级别和存储方式
  4. 安全性:避免在错误消息中泄露敏感信息
  5. 性能考虑:异常处理应尽量轻量,避免影响系统性能

常见问题

Q: 如何排除特定路由不使用全局过滤器?

A: 可以在过滤器中添加路由排除逻辑:

typescript
catch(exception: unknown, host: ArgumentsHost) {
  const ctx = host.switchToHttp()
  const request = ctx.getRequest<Request>()
  
  // 排除健康检查等特定路由
  if (request.url.includes('/health')) {
    throw exception // 重新抛出,让默认处理器处理
  }
  
  // 正常处理逻辑...
}

Q: 如何处理不同类型的数据库异常?

A: 扩展异常处理逻辑:

typescript
if (exception.code === 'ER_DUP_ENTRY') {
  // 处理重复条目
} else if (exception.code === 'ER_NO_REFERENCED_ROW_2') {
  // 处理外键约束错误
  message = '关联数据不存在'
} else if (exception.code === 'ER_DATA_TOO_LONG') {
  // 处理数据超长错误
  message = '数据长度超出限制'
}

Q: 如何在不同环境下返回不同的错误信息?

A: 根据环境变量调整错误信息:

typescript
const isProduction = process.env.NODE_ENV === 'production'
const errorMessage = isProduction 
  ? '服务器内部错误' 
  : exception.message

Q: 如何记录异常堆栈信息?

A: 在开发环境中记录详细信息:

typescript
if (process.env.NODE_ENV === 'development') {
  console.error('Exception stack:', exception.stack)
}

Q: 如何与现有的错误处理机制集成?

A: 可以保持现有处理逻辑的同时添加全局过滤器:

typescript
// 保留控制器中的 try-catch
@Post()
async create(@Body() dto: CreateDto) {
  try {
    return await this.service.create(dto)
  } catch (error) {
    // 特定业务逻辑处理
    if (error.code === 'CUSTOM_ERROR') {
      throw new HttpException('自定义错误', HttpStatus.BAD_REQUEST)
    }
    throw error // 其他异常交给全局过滤器处理
  }
}

配置示例

完整的 main.ts 配置

typescript
import { NestFactory } from '@nestjs/core'
import { ValidationPipe } from '@nestjs/common'
import { AppModule } from './app.module'
import { GlobalExceptionsFilter } from './common/filters/GlobalExceptionsFilter'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  
  // 全局管道配置
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }))
  
  // 全局异常过滤器
  app.useGlobalFilters(new GlobalExceptionsFilter())
  
  // CORS 配置
  app.enableCors()
  
  await app.listen(process.env.PORT || 3000)
}
bootstrap()