第 7 章:异常过滤器
学习目标
- 理解异常处理流程
- 使用内置 HttpException
- 写自定义异常过滤器
- 避开 3 个异常处理坑
一、默认异常机制
NestJS 抛 throw new Error(...) 默认 500,内置 HTTP 异常:
| 异常 | 状态码 |
|---|---|
BadRequestException | 400 |
UnauthorizedException | 401 |
ForbiddenException | 403 |
NotFoundException | 404 |
ConflictException | 409 |
InternalServerErrorException | 500 |
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,不依赖 Serviceproviders+APP_FILTER—— 需要注入 Service(日志、配置)时必须用这个
APP_FILTER 全局生效原理:
typescript
import { APP_FILTER, APP_GUARD, APP_PIPE, APP_INTERCEPTOR } from '@nestjs/core';
// ^^^ 从 @nestjs/core 导入,不是 @nestjs/commonAPP_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 |
动手练习
- 统一格式:写一个全局过滤器,把响应改成
{ code, msg, data } - 自定义异常:写
BusinessException,在 Service 里抛一个 - 日志:在过滤器里把异常 stack 用 Logger 打到文件
下一章:第 8 章:管道 →