如何在 NestJS 项目中统一处理 Controller 层异步异常响应?

文章导读
在 NestJS 项目中,最推荐的做法是组合使用全局异常过滤器(Exception Filter)捕获错误,配合拦截器(Interceptor)统一成功响应格式。这样既能保证异步错误不泄露堆栈,又能让前后端接口结构一致,同时保留正确的 HTTP 状态码语义。
📋 目录
  1. 核心代码实现
  2. 验证方法
  3. 常见坑与排查
  4. 参考文档
A A

在 NestJS 项目中,最推荐的做法是组合使用全局异常过滤器(Exception Filter)捕获错误,配合拦截器(Interceptor)统一成功响应格式。这样既能保证异步错误不泄露堆栈,又能让前后端接口结构一致,同时保留正确的 HTTP 状态码语义。

先说结论:通过全局过滤器拦截所有未处理异常并转换为标准 JSON,再用拦截器包裹正常返回值,实现全链路响应统一。

  • 适合:需要统一前端错误提示码、隐藏服务端内部错误详情的中大型项目。
  • 关键点:过滤器必须区分 HttpException 与普通 Error,避免将 404/400 等客户端错误统一转为 500。
  • 建议:自定义业务异常类继承 HttpException,以便在过滤器中区分业务错误与系统错误。

核心代码实现

以下是经过修正的完整代码结构,包含必要的 import 语句、状态码判断逻辑以及响应拦截器实现。

1. 自定义业务异常类

为了方便抛出业务错误,建议封装一个基础异常类。

// common/exceptions/business.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class BusinessException extends HttpException {
  constructor(message: string, status: HttpStatus = HttpStatus.BAD_REQUEST) {
    super(message, status);
  }
}

2. 全局异常过滤器

修复了强制返回 500 的问题,增加了对 HttpException 实例的判断,保留原始状态码。

如何在 NestJS 项目中统一处理 Controller 层异步异常响应?
// common/filters/all-exceptions.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';

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

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = '系统繁忙,请稍后重试';

    // 区分 HttpException 与其他错误,避免掩盖 404/400 等状态
    if (exception instanceof HttpException) {
      status = exception.getStatus();
      message = exception.getMessage();
    }

    response.status(status).json({
      code: status,
      message,
      data: null,
      path: request.url,
    });
  }
}

3. 统一响应拦截器

使用 RxJS 的 map 操作符包裹正常返回数据,确保成功响应的结构统一。

// common/interceptors/transform-response.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response {
  code: number;
  message: string;
  data: T;
}

@Injectable()
export class TransformResponseInterceptor implements NestInterceptor> {
  intercept(context: ExecutionContext, next: CallHandler): Observable> {
    return next.handle().pipe(
      map((data) => ({
        code: 200,
        message: 'success',
        data,
      })),
    );
  }
}

4. 全局注册

在 main.ts 中完成注册,确保覆盖所有模块。

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
import { TransformResponseInterceptor } from './common/interceptors/transform-response.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 注册全局异常过滤器
  app.useGlobalFilters(new AllExceptionsFilter());
  // 注册全局响应拦截器
  app.useGlobalInterceptors(new TransformResponseInterceptor());
  await app.listen(3000);
}
bootstrap();

验证方法

完成代码部署后,通过以下命令验证异常处理是否生效且状态码正确。

1. 验证系统异常(500)

如何在 NestJS 项目中统一处理 Controller 层异步异常响应?

在 Controller 中故意抛出普通 Error,检查是否被捕获且不带堆栈信息。

curl -v http://localhost:3000/api/test-error

预期结果:HTTP 状态码 500,响应体包含统一 code 和 message,无 stack 字段。

2. 验证业务异常(400/404)

抛出 BusinessException 或 HttpException,检查状态码是否保持原样。

如何在 NestJS 项目中统一处理 Controller 层异步异常响应?
curl -v http://localhost:3000/api/test-not-found

预期结果:HTTP 状态码 404 或 400,响应体结构与其他错误一致。

3. 验证正常响应

请求正常接口,检查是否被拦截器包裹。

curl -v http://localhost:3000/api/test-success

预期结果:HTTP 状态码 200,响应体结构为 { code: 200, message: 'success', data: ... }。

常见坑与排查

  • 过滤器顺序问题:如果同时使用了局部过滤器和全局过滤器,局部优先于全局。确保全局过滤器能兜底未捕获的异常。
  • 异步错误未捕获:在 Service 层调用异步方法时,如果没有 await 或者没有 try-catch,错误可能会 bypass 某些逻辑。确保所有异步操作都被正确 awaited,或者让错误自然抛出由全局过滤器捕获。
  • RxJS 错误流:拦截器中如果使用 RxJS 操作符,注意错误流不会被 map 捕获。如果需要拦截器处理错误,需使用 catchError 操作符,但通常建议错误交给过滤器处理。
  • 响应体被多次修改:过滤器只负责错误,拦截器只负责成功响应的格式转换。不要在过滤器中修改成功响应,也不要在拦截器中处理异常逻辑,避免冲突。

参考文档