Skip to content

什么是异常过滤器?🤔 ​

异常过滤器(Exception Filter)是 NestJS 中处理异常的机制,它可以捕获整个应用程序中抛出的异常,并处理这些异常。通过异常过滤器,我们可以集中处理错误,统一错误响应格式,并进行适当的错误日志记录。

Express vs NestJS 对比 🆚 ​

让我们看看在传统 Express 和 NestJS 中如何处理异常:

Express 写法:

javascript
// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  
  // 处理不同类型的错误
  if (err instanceof ValidationError) {
    return res.status(400).json({
      code: 400,
      message: '验证错误',
      errors: err.errors
    });
  }
  
  if (err.name === 'UnauthorizedError') {
    return res.status(401).json({
      code: 401,
      message: '未授权'
    });
  }
  
  // 默认错误响应
  res.status(500).json({
    code: 500,
    message: '服务器内部错误'
  });
});

NestJS 写法:

typescript
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const status = 
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception instanceof HttpException
        ? exception.message
        : '服务器内部错误'
    });
  }
}

内置异常 🎯 ​

NestJS 提供了一系列内置的 HTTP 异常类:

typescript
// 400 Bad Request
throw new BadRequestException('参数验证失败');

// 401 Unauthorized
throw new UnauthorizedException('未授权');

// 403 Forbidden
throw new ForbiddenException('权限不足');

// 404 Not Found
throw new NotFoundException('资源不存在');

// 429 Too Many Requests
throw new ThrottlerException('请求过于频繁');

// 500 Internal Server Error
throw new InternalServerErrorException('服务器错误');

自定义异常过滤器 🛠️ ​

1. 基本异常过滤器 ​

typescript
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  constructor(private readonly logger: Logger) {}

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    const errorResponse = {
      code: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      message: exception.message || null,
    };

    this.logger.error(
      `${request.method} ${request.url}`,
      exception.stack,
      'ExceptionFilter'
    );

    response
      .status(status)
      .json(errorResponse);
  }
}

2. 业务异常过滤器 ​

typescript
// exceptions/business.exception.ts
export class BusinessException extends HttpException {
  constructor(
    public readonly code: string,
    message: string,
    status: HttpStatus = HttpStatus.BAD_REQUEST
  ) {
    super({ code, message }, status);
  }
}

// filters/business-exception.filter.ts
@Catch(BusinessException)
export class BusinessExceptionFilter implements ExceptionFilter {
  catch(exception: BusinessException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();

    response.status(exception.getStatus()).json({
      code: exception.code,
      message: exception.message,
      timestamp: new Date().toISOString()
    });
  }
}

3. 数据库异常过滤器 ​

typescript
@Catch(TypeORMError)
export class DatabaseExceptionFilter implements ExceptionFilter {
  constructor(private readonly logger: Logger) {}

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

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = '数据库错误';

    // 处理不同类型的数据库错误
    if (exception instanceof QueryFailedError) {
      if (exception.message.includes('duplicate key')) {
        status = HttpStatus.CONFLICT;
        message = '数据已存在';
      }
    }

    this.logger.error(
      `Database error: ${exception.message}`,
      exception.stack,
      'DatabaseExceptionFilter'
    );

    response.status(status).json({
      statusCode: status,
      message,
      timestamp: new Date().toISOString(),
      path: request.url
    });
  }
}

高级用法 🚀 ​

1. 全局异常过滤器 ​

typescript
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  const logger = new Logger();
  
  app.useGlobalFilters(
    new HttpExceptionFilter(logger),
    new BusinessExceptionFilter(),
    new DatabaseExceptionFilter(logger)
  );
  
  await app.listen(3000);
}

2. 带有依赖注入的异常过滤器 ​

typescript
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(
    private readonly httpAdapterHost: HttpAdapterHost,
    private readonly logger: Logger,
    private readonly configService: ConfigService
  ) {}

  catch(exception: unknown, host: ArgumentsHost) {
    const { httpAdapter } = this.httpAdapterHost;
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();

    const httpStatus =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(request),
      message: this.getExceptionMessage(exception),
      stack: this.shouldIncludeStack(exception)
        ? this.getExceptionStack(exception)
        : undefined
    };

    this.logException(exception, request);
    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
  }

  private getExceptionMessage(exception: unknown): string {
    if (exception instanceof HttpException) {
      return exception.message;
    }
    return '服务器内部错误';
  }

  private getExceptionStack(exception: unknown): string | undefined {
    if (exception instanceof Error) {
      return exception.stack;
    }
    return undefined;
  }

  private shouldIncludeStack(exception: unknown): boolean {
    return this.configService.get('NODE_ENV') === 'development';
  }

  private logException(exception: unknown, request: Request): void {
    const message = this.getExceptionMessage(exception);
    const stack = this.getExceptionStack(exception);
    
    this.logger.error(
      `Exception occurred in ${request.method} ${request.url}: ${message}`,
      stack,
      'AllExceptionsFilter'
    );
  }
}

实战示例 💡 ​

1. API 错误处理系统 ​

typescript
// interfaces/api-error.interface.ts
export interface ApiError {
  code: string;
  message: string;
  details?: any;
}

// exceptions/api.exception.ts
export class ApiException extends HttpException {
  constructor(error: ApiError, status: HttpStatus = HttpStatus.BAD_REQUEST) {
    super(error, status);
  }
}

// filters/api-exception.filter.ts
@Catch(ApiException)
export class ApiExceptionFilter implements ExceptionFilter {
  constructor(private readonly logger: Logger) {}

  catch(exception: ApiException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const error = exception.getResponse() as ApiError;

    const errorResponse = {
      ...error,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method
    };

    this.logger.error(
      `API Error: ${error.message}`,
      error.details,
      'ApiExceptionFilter'
    );

    response.status(status).json(errorResponse);
  }
}

最佳实践 ✨ ​

1. 错误代码管理 ​

typescript
// constants/error-codes.ts
export const ErrorCodes = {
  VALIDATION_ERROR: 'E0001',
  UNAUTHORIZED: 'E0002',
  FORBIDDEN: 'E0003',
  NOT_FOUND: 'E0004',
  INTERNAL_ERROR: 'E0005',
  // ... 更多错误代码
} as const;

// 使用错误代码
throw new ApiException({
  code: ErrorCodes.VALIDATION_ERROR,
  message: '验证失败',
  details: validationErrors
});

2. 错误响应格式统一 ​

typescript
interface StandardError {
  code: string;
  message: string;
  details?: any;
  timestamp: string;
  path: string;
  requestId?: string;
}

@Catch()
export class StandardExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost): void {
    // ... 实现标准错误响应格式
  }
}

总结 📝 ​

通过这篇文章,我们深入学习了:

  1. 异常过滤器的基本概念和作用
  2. NestJS 内置的异常类型
  3. 如何创建和使用自定义异常过滤器
  4. 异常处理的最佳实践
  5. 实际项目中的错误处理系统

异常过滤器是 NestJS 中处理错误的强大工具,它可以帮助我们:

  • 统一处理异常
  • 标准化错误响应
  • 集中化错误日志
  • 提供更好的错误追踪
  • 改善应用程序的可维护性

下一篇文章,我们将探讨 NestJS 的配置系统(Configuration)。

相关资源 📚 ​