Skip to content
第 7 章 后端 ⏱ 13 分钟阅读

第 7 章:异常过滤器 ​

学习目标 ​

  • 理解异常处理流程
  • 使用内置 HttpException
  • 写自定义异常过滤器
  • 避开 3 个异常处理坑

一、默认异常机制 ​

NestJS 抛 throw new Error(...) 默认 500,内置 HTTP 异常:

异常状态码
BadRequestException400
UnauthorizedException401
ForbiddenException403
NotFoundException404
ConflictException409
InternalServerErrorException500
typescript
@Get(':id')
findOne(@Param('id') id: string) {
  const user = this.svc.findOne(+id);
  if (!user) throw new NotFoundException(`user ${id} not found`);
  return user;
}

返回:

json
{ "statusCode": 404, "message": "user 42 not found", "error": "Not Found" }

二、抛自定义 message ​

typescript
throw new BadRequestException({
  code: 'INVALID_EMAIL',
  message: '邮箱格式错误',
  fields: { email: 'must contain @' },
});

响应:

json
{ "code": "INVALID_EMAIL", "message": "...", "fields": {...}, "statusCode": 400 }

三、自定义异常类 ​

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

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

// 使用
throw new BusinessException('USER_EXISTS', '用户已存在');

四、异常过滤器(改响应格式) ​

typescript
// filters/all-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()                                        // 不传参 = 捕获所有
export class AllExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse<Response>();
    const req = ctx.getRequest<Request>();

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

    const body = {
      code: status,
      message: this.getMessage(exception),
      path: req.url,
      timestamp: Date.now(),
    };

    res.status(status).json(body);
  }

  private getMessage(ex: unknown): string {
    if (ex instanceof HttpException) {
      const r = ex.getResponse();
      return typeof r === 'string' ? r : (r as any).message ?? ex.message;
    }
    return (ex as Error)?.message ?? 'Internal error';
  }
}

注册(全局):

typescript
// main.ts
app.useGlobalFilters(new AllExceptionFilter());

或者用 Module:

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

⚠️ 坑 1:useGlobalFilters 注册的过滤器不能用 DI,要用 APP_FILTER 才能注入 Service。

坑 1 详解:

typescript
// main.ts
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(MyFilter);  // ← 不能注入 Service

注册时机问题:NestFactory.create() 已经把整个 Module 树、DI 容器初始化完了,这时才 useGlobalFilters(),过滤器脱离了 DI 生命周期,构造器注入失效:

typescript
@Catch()
export class MyFilter implements ExceptionFilter {
  constructor(private svc: SomeService) {} // ❌ 拿不到 svc
}

APP_FILTER 注册走的是正常 Module DI 流程,可以注入:

typescript
@Module({
  providers: [
    LogService,
    { provide: APP_FILTER, useClass: MyFilter }, // ← 走 DI 容器
  ],
})

export class AppModule {}

@Catch()
export class MyFilter implements ExceptionFilter {
  constructor(private svc: LogService) {} // ✅ 能拿到 svc
}

记忆口诀:

  • main.ts + useGlobalFilters —— 轻量过滤器,只用 ArgumentsHost,不依赖 Service
  • providers + APP_FILTER —— 需要注入 Service(日志、配置)时必须用这个

APP_FILTER 全局生效原理:

typescript
import { APP_FILTER, APP_GUARD, APP_PIPE, APP_INTERCEPTOR } from '@nestjs/core';
//                                  ^^^ 从 @nestjs/core 导入,不是 @nestjs/common

APP_FILTER 是 NestJS 框架内部的特殊多 Provider token,框架会自动识别并标记为"全局生效":

1. AppModule 启动 → 注册 AllExceptionFilter 到 providers
   ↓
2. NestJS 核心(@nestjs/core)扫描所有 providers
   ↓
3. 发现 APP_FILTER token → 标记为全局拦截器
   ↓
4. 所有 Module 的 Controller 抛异常时,自动走 AllExceptionFilter
   (不只 AppModule,UsersModule / OrdersModule 都生效)

完整 APP_* 系列:

typescript
providers: [
  { provide: APP_FILTER, useClass: AllExceptionFilter },     // 全局异常过滤器
  { provide: APP_GUARD, useClass: JwtAuthGuard },           // 全局守卫
  { provide: APP_PIPE, useClass: ValidationPipe },          // 全局管道
  { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, // 全局拦截器
]

对比普通 Provider:

Provider作用域
UsersService(普通类)只在当前 Module / imports 进来的 Module
AllExceptionFilter(用 APP_FILTER)整个应用所有 Controller 都生效

放在 AppModule 只是惯例,放任何 Module 注册都生效。

五、按异常类型捕获 ​

typescript
@Catch(HttpException)                            // 只捕获 HTTP 异常
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    // ...
  }
}

@Catch(BusinessException)                        // 捕自定义异常
export class BusinessFilter implements ExceptionFilter { /* ... */ }

六、@Catch 优先级 ​

NestJS 按最具体优先:先匹配自定义异常,再匹配父类,最后匹配 @Catch()。

⚠️ 坑 2:在 @Catch() 里不判断类型直接拿 .message,会拿到 "connect ECONNREFUSED" 这类系统错误。

七、在 Controller 里抛 vs 拦截器 ​

  • Controller/Service:throw new NotFoundException(...)
  • Interceptor:改返回值或包装错误
  • Filter:改输出格式,不要在这里做业务逻辑

八、实战:统一异常响应 ​

typescript
@Catch()
export class GlobalFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse();
    const req = ctx.getRequest();

    let code = 500;
    let msg = 'Internal Server Error';

    if (exception instanceof HttpException) {
      code = exception.getStatus();
      const r = exception.getResponse();
      msg = typeof r === 'string' ? r : (r as any).message;
    } else if (exception instanceof Error) {
      msg = exception.message;
    }

    res.status(code).json({
      success: false,
      code,
      message: msg,
      path: req.url,
      ts: new Date().toISOString(),
    });
  }
}

⚠️ 坑 3:生产环境不要把 exception.stack 返回给前端,会泄露源码。

九、404 处理 ​

Controller 没匹配的路由会走 NotFoundException,过滤器自动接住。

十、本章小结 ​

要点关键
内置异常Bad/Unauthorized/Forbidden/NotFound/ConflictException
自定义异常继承 HttpException
过滤器@Catch() + 实现 ExceptionFilter
全局注册useGlobalFilters 或 APP_FILTER
注入用 APP_FILTER 才能 DI

动手练习 ​

  1. 统一格式:写一个全局过滤器,把响应改成 { code, msg, data }
  2. 自定义异常:写 BusinessException,在 Service 里抛一个
  3. 日志:在过滤器里把异常 stack 用 Logger 打到文件

下一章:第 8 章:管道 →

本站基于 VitePress 构建 · 由 StackHub 团队维护