Skip to content

异常过滤器

闯关 NestJS 异常过滤器:优雅地处理错误

想象一下,在你的应用中,用户尝试获取一个不存在的资源,或者在创建数据时格式不正确。如果没有妥善处理,服务器可能会崩溃,或者返回一堆对用户来说毫无意义的、丑陋的错误代码。这显然不是我们想要的。

异常过滤器就是你的应用的“危机公关专家”。它的核心职责是:捕获在应用中抛出的未被处理的异常,并根据你的逻辑,将其转换成一个对用户友好的、格式统一的响应

1. 为什么需要异常过滤器?

在代码中,我们经常使用 throw new Error() 来中断一个不正常的执行流程。在 NestJS 中,有一个内置的 HttpException 类,专门用来处理与 HTTP 请求相关的错误。

问题来了: 如果我们不“捕获”这些被抛出的异常,会发生什么?

  • 程序崩溃:对于一些未知的错误,可能会导致 Node.js 进程退出。
  • 响应不友好:NestJS 默认会捕获异常并返回一个 JSON 响应,但格式可能不是你想要的。例如:
    json
    {
      "statusCode": 404,
      "message": "Cannot GET /non-existent-route",
      "error": "Not Found"
    }
  • 响应格式不统一:你自己抛出的业务错误和框架抛出的错误,返回的格式可能五花八门,这对于前端开发者来说是个噩梦。

异常过滤器的目标:无论哪里发生了错误(无论是 NestJS 内部抛出的,还是你自己代码里 throw 的),都通过一个统一的出口进行处理,最终给客户端返回一个结构一致的、清晰的错误信息。

2. 核心武器:HttpException

在深入自定义过滤器之前,我们必须先了解它最常处理的对象:HttpException

HttpException 是 NestJS 提供的一个基础异常类。当你需要表示一个标准的 HTTP 错误时(如 404 Not Found, 403 Forbidden),你应该优先使用它或它的子类。

基础用法:

typescript
import { HttpException, HttpStatus } from '@nestjs/common';

// 在你的 Controller 或 Service 中
@Get(':id')
findOne(@Param('id') id: string) {
  // 假设我们找不到 id 对应的猫
  if (!cat) {
    throw new HttpException('我们找不到这只猫', HttpStatus.NOT_FOUND);
  }
  return cat;
}

你甚至可以传递一个对象作为第一个参数,来覆盖整个响应体:

typescript
throw new HttpException({
  status: HttpStatus.FORBIDDEN,
  error: '这是一个自定义的错误消息',
}, HttpStatus.FORBIDDEN);

NestJS 还内置了很多继承自 HttpException 的常用异常类,让代码更具可读性:

  • BadRequestException (400)
  • UnauthorizedException (401)
  • NotFoundException (404)
  • ForbiddenException (403)
  • ...等等

所以,上面的例子可以写得更优雅:

typescript
import { NotFoundException } from '@nestjs/common';

// 在你的 Controller 或 Service 中
@Get(':id')
findOne(@Param('id') id: string) {
  if (!cat) {
    throw new NotFoundException('我们找不到这只猫'); // 代码更清晰!
  }
  return cat;
}

3. 创建你的第一个异常过滤器

现在,让我们创建一个专门捕获所有 HttpException 类型错误的过滤器,并自定义返回的 JSON 格式。

src/filters/http-exception.filter.ts

typescript
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException) // 1. 指定只捕获 HttpException 类型的异常
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    // 2. catch 方法是核心实现
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus(); // 获取异常的状态码
    const message = exception.message; // 获取异常的消息

    // 3. 自定义你想要的返回格式
    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
        message: message,
      });
  }
}

让我们来分解一下这段代码:

  1. @Catch(HttpException):这是一个装饰器,它告诉 NestJS,这个过滤器只关心类型为 HttpException(或其子类)的异常。如果留空 @Catch(),它会捕获所有的异常。
  2. catch(exception, host):这是 ExceptionFilter 接口要求必须实现的方法。
    • exception: 当前正在处理的异常对象,也就是我们 throw 出来的那个。
    • host: ArgumentsHost 是一个非常强大的工具类。简单来说,它是一个参数的包装器,可以帮助我们获取到原始的请求处理函数的参数,无论当前是 HTTP、WebSockets 还是 gRPC 上下文。在这里,我们用 host.switchToHttp() 来明确获取 HTTP 上下文,并从中拿到 requestresponse 对象。
  3. 自定义响应:我们从异常对象中获取状态码 (status) 和消息 (message),然后结合请求的 URL 和时间戳,构造了一个全新的、结构化的 JSON 对象,并将其发送回客户端。

4. 如何应用异常过滤器?

创建好过滤器后,我们需要告诉 NestJS 在哪里使用它。你有三种“作用域”可以选择:

方式一:方法作用域 (Method-scoped)

只对单个路由处理函数生效。

typescript
import { HttpExceptionFilter } from '../filters/http-exception.filter';

@Post()
@UseFilters(new HttpExceptionFilter()) // 在这里应用
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException('这是一条只会被 HttpExceptionFilter 捕获的消息');
}

方式二:控制器作用域 (Controller-scoped)

对一个控制器内的所有路由处理函数生效。

typescript
import { HttpExceptionFilter } from '../filters/http-exception.filter';

@Controller('cats')
@UseFilters(new HttpExceptionFilter()) // 在这里应用
export class CatsController {
  // ... 这个控制器下的所有方法都会应用此过滤器
}

方式三:全局作用域 (Global-scoped)

对整个应用的所有路由生效。这是最常用的方式,可以确保整个应用的错误响应格式统一。

main.ts 文件中进行设置:

src/main.ts

typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 注册为全局过滤器
  app.useGlobalFilters(new HttpExceptionFilter());

  await app.listen(3000);
}
bootstrap();

注意:通过 app.useGlobalFilters() 注册的过滤器无法使用依赖注入 (DI),因为它是在任何模块上下文之外注册的。如果你的过滤器需要依赖注入(比如注入一个日志服务),请看下面的进阶用法。

5. 进阶用法

捕获所有异常

通常一个健壮的应用需要至少两个全局过滤器:一个处理 HttpException,另一个处理所有其他意料之外的错误(例如,代码中的 TypeError)。

我们可以创建一个“万能”过滤器,只需将 @Catch() 装饰器留空即可。

src/filters/any-exception.filter.ts

typescript
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';

@Catch() // 留空以捕获所有类型的异常
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    // 判断异常是否是 HttpException 的实例
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR; // 如果不是,则统一视为500服务器内部错误

    // 在生产环境中,你可能不想暴露原始的错误堆栈信息
    const message =
      exception instanceof HttpException
        ? exception.getResponse()
        : 'Internal server error';

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

最佳实践:通常,将处理特定类型(如 HttpExceptionFilter)的过滤器和处理所有其他异常的“万能”过滤器结合使用,可以使你的错误处理逻辑更加清晰。NestJS 会先检查更具体的绑定,所以 HttpExceptionFilter 会优先于 AllExceptionsFilter 被调用来处理 HttpException

使用依赖注入

如果你的过滤器需要记录错误日志,你可能需要注入一个 LoggerService。这时,就不能使用 app.useGlobalFilters() 了,而是需要通过模块的 providers 来注册。

src/app.module.ts

typescript
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AllExceptionsFilter } from './filters/any-exception.filter';

@Module({
  providers: [
    {
      provide: APP_FILTER, // 这是一个特殊的令牌,告诉 NestJS 这是一个全局过滤器
      useClass: AllExceptionsFilter, // 使用我们的过滤器类
    },
    // 如果你的 AllExceptionsFilter 依赖了某个服务,
    // 比如 LoggerService,你也需要在这里提供它。
    // LoggerService,
  ],
})
export class AppModule {}

通过这种方式注册的全局过滤器,就可以在它的构造函数中正常使用依赖注入了。

总结

异常过滤器是 NestJS 中构建健壮、可靠应用的关键一环。它为你提供了一个集中的地方来处理所有错误,确保了无论发生什么,你的应用都能以一种可预测的、优雅的方式做出响应。

回顾一下关键点:

  • 目的:捕获未处理的异常,返回统一、友好的错误响应。
  • 核心类:优先使用 HttpException 及其子类来抛出业务逻辑错误。
  • 创建:实现 ExceptionFilter 接口,并使用 @Catch() 装饰器。
  • 应用:可以在方法、控制器或全局三个级别上应用。
  • 全局应用:推荐使用 app.useGlobalFilters() (无 DI) 或 APP_FILTER 提供者 (有 DI)。

现在,你已经掌握了处理应用“异常情况”的超能力了!