NestJS异常过滤器学习笔记
NestJS异常过滤器学习笔记
核心知识点
1. 日志方案回顾与选型建议
1.1 Winston vs Pino 优缺点总结
| 方案 | 优点 | 缺点 | 推荐人群 |
|---|---|---|---|
| Winston | daily-rotate-file 功能详细、稳定、开箱即用 | 需要手动在每个位置加日志 | 追求稳定、功能全面 |
| Pino | 开箱即用、性能极高(很多大神推荐) | 文件滚动功能不如 Winston 完善 | 比较懒、对性能有要求 |
1.2 手动加日志的痛点
typescript
// 每个接口都要手动 try-catch,非常繁琐
@Get()
async getUsers() {
try {
return await this.userService.findAll();
} catch (error) {
this.logger.error('获取用户列表失败', error.stack);
throw error;
}
}
@Post()
async createUser(@Body() body: any) {
try {
return await this.userService.create(body);
} catch (error) {
this.logger.error('创建用户失败', error.stack);
throw error;
}
}解决思路:使用 NestJS 的 异常过滤器(Exception Filter),在全局层面统一捕获和记录异常。
2. NestJS 异常处理机制
2.1 异常处理流程
code
客户端请求
↓
路由层(Route)
↓
控制器层(Controller)
↓
服务层(Service)
↓
┌─────────────────────────┐
│ 发生异常? │
│ ↓ Yes │
│ 异常被抛出 │
│ ↓ │
│ Exception Filter 捕获 │
│ ├── 路由过滤器(作用域:单个路由)
│ ├── 控制器过滤器(作用域:整个控制器)
│ └── 全局过滤器(作用域:整个应用) ← 本节重点
│ ↓ │
│ 返回 HTTP 响应给前端 │
└─────────────────────────┘2.2 三种异常过滤器
| 过滤器类型 | 作用范围 | 执行顺序 | 优先级 |
|---|---|---|---|
| 全局过滤器(Global Filter) | 整个应用所有路由 | 最后执行 | 最低(兜底) |
| 控制器过滤器(Controller Filter) | 当前控制器下所有路由 | 中间执行 | 中 |
| 路由过滤器(Route Filter) | 单个路由 | 最先执行 | 最高 |
2.3 在请求生命周期中的位置
code
客户端请求
↓
Middleware(中间件)
↓
Guard(守卫)
↓
Interceptor(拦截器)— 前置
↓
Pipe(管道)
↓
Controller + Service(业务逻辑)
↓
Interceptor(拦截器)— 后置
↓
Exception Filter(异常过滤器)← 生命周期末端
↓
响应给客户端异常过滤器处于生命周期的末端,所有异常最终都会经过这里。
3. 主动抛出异常
3.1 使用 HttpException
typescript
import { HttpException, HttpStatus } from '@nestjs/common';
@Get('users')
getUsers() {
const user = { isAdmin: false };
if (!user.isAdmin) {
throw new HttpException(
'User is not admin, forbidden to access get all users',
HttpStatus.FORBIDDEN, // 403
);
}
return { users: [] };
}Postman 响应:
json
{
"statusCode": 403,
"message": "User is not admin, forbidden to access get all users",
"error": "Forbidden"
}3.2 使用内置异常类(推荐)
NestJS 提供了大量语义化的内置异常类,无需手动指定状态码:
typescript
import {
BadRequestException, // 400
UnauthorizedException, // 401
ForbiddenException, // 403
NotFoundException, // 404
InternalServerErrorException, // 500
} from '@nestjs/common';使用示例:
typescript
// 404 — 用户不存在
throw new NotFoundException('用户不存在');
// 401 — 未授权
throw new UnauthorizedException('用户没有权限');
// 400 — 请求参数错误
throw new BadRequestException('请求参数不合法');3.3 常用内置异常类速查
| 异常类 | 状态码 | 说明 |
|---|---|---|
BadRequestException | 400 | 请求参数错误 |
UnauthorizedException | 401 | 未认证/未授权 |
ForbiddenException | 403 | 禁止访问 |
NotFoundException | 404 | 资源不存在 |
MethodNotAllowedException | 405 | 请求方法不允许 |
RequestTimeoutException | 408 | 请求超时 |
ConflictException | 409 | 资源冲突 |
InternalServerErrorException | 500 | 服务器内部错误 |
NotAcceptableException | 406 | 不接受的响应格式 |
ServiceUnavailableException | 503 | 服务不可用 |
所有异常类都可通过
Cmd/Ctrl + Click跳转到源码,查看更多异常类及其说明。
4. 自定义全局异常过滤器
4.1 创建过滤器
typescript
// src/filters/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { LogService } from '../log/log.service';
@Catch(HttpException) // 指定捕获的异常类型
export class HttpExceptionFilter implements ExceptionFilter {
constructor(private readonly logger: LogService) {}
catch(exception: HttpException, host: ArgumentsHost) {
// 获取上下文
const ctx = host.switchToHttp();
// 获取请求和响应对象
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
// 获取 HTTP 状态码
const status = exception.getStatus();
// 构造响应数据
const errorResponse = {
code: status,
timestamp: new Date().toISOString(),
message: exception.message || exception.name,
};
// 记录错误日志
this.logger.error(
`${request.method} ${request.url} - ${errorResponse.message}`,
exception.stack,
);
// 返回自定义响应
response.status(status).json(errorResponse);
}
}4.2 代码逐行解析
typescript
@Catch(HttpException)
// @Catch() 装饰器:指定要捕获的异常类型
// HttpException:捕获所有 HTTP 异常
// 如果写 @Catch() 不传参数,则捕获所有异常
class HttpExceptionFilter implements ExceptionFilter {
// implements ExceptionFilter:TypeScript 接口约束
// 必须实现 catch() 方法
catch(exception: HttpException, host: ArgumentsHost) {
// exception:捕获到的异常对象
// host:参数宿主,可以获取到请求上下文
const ctx = host.switchToHttp();
// switchToHttp():切换到 HTTP 上下文
// 类似 Express 中的 req/res 中间件
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
// 获取 Express 的 request 和 response 对象
const status = exception.getStatus();
// 获取 HTTP 状态码(如 404、403、500)
response.status(status).json({ ... });
// 自定义响应格式返回给前端
}
}4.3 ArgumentsHost(参数宿主)详解
code
ArgumentsHost
├── switchToHttp() → HTTP 上下文(req / res)
│ ├── getRequest() → Express Request 对象
│ └── getResponse() → Express Response 对象
│
├── switchToRpc() → RPC 上下文(微服务)
│ └── getData() → RPC 数据
│
└── switchToWs() → WebSocket 上下文
├── getClient() → WebSocket 客户端
└── getData() → WebSocket 数据
ArgumentsHost是 NestJS 的核心概念,它允许同一个过滤器在不同的传输层(HTTP/RPC/WebSocket)中工作。
5. 注册全局过滤器
5.1 在 main.ts 中注册
typescript
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 注册全局异常过滤器
app.useGlobalFilters(
new HttpExceptionFilter(app.get(WINSTON_MODULE_NEST_PROVIDER)),
);
await app.listen(3000);
}
bootstrap();
app.get(WINSTON_MODULE_NEST_PROVIDER)从 DI 容器中获取 Logger 实例,注入到过滤器中。
5.2 注意事项
- 全局 Exception Filter 只能有一个:如果注册了多个,后面的会覆盖前面的
- 如果需要捕获非 HTTP 异常(如 WebSocket),可以创建单独的过滤器
6. 过滤器执行效果
6.1 请求不存在的路由
请求:GET /api/v1/not-exist
默认响应(无自定义过滤器):
json
{
"statusCode": 404,
"message": "Cannot GET /api/v1/not-exist",
"error": "Not Found"
}自定义过滤器响应:
json
{
"code": 404,
"timestamp": "2026-03-30T08:00:00.000Z",
"message": "Cannot GET /api/v1/not-exist"
}6.2 异常对象可用的信息
typescript
exception.message // 错误消息:"Cannot GET /api/v1/not-exist"
exception.name // 异常名称:"NotFoundException"
exception.getStatus() // HTTP 状态码:404
exception.stack // 完整堆栈信息(用于日志记录)
exception.getResponse() // 完整响应对象6.3 日志记录效果
logs/application-2026-03-30.log 中自动记录:
code
2026-03-30T08:00:00.000Z error: GET /api/v1/not-exist - Cannot GET /api/v1/not-exist
Error: Cannot GET /api/v1/not-exist
at ...(完整堆栈信息)7. 进阶:捕获所有异常
7.1 @Catch 不传参数
typescript
@Catch() // 不传参数 = 捕获所有异常(包括非 HTTP 异常)
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
// 判断是否为 HTTP 异常
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.message
: 'Internal server error';
response.status(status).json({
code: status,
timestamp: new Date().toISOString(),
message,
path: request.url,
});
}
}7.2 WebSocket 异常过滤器(课后作业)
typescript
@Catch(WsException) // 捕获 WebSocket 异常
export class WsExceptionFilter implements ExceptionFilter {
catch(exception: WsException, host: ArgumentsHost) {
const ctx = host.switchToWs();
const client = ctx.getClient();
const data = ctx.getData();
client.emit('error', {
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}代码实战案例
需求描述
创建自定义全局异常过滤器,统一捕获所有 HTTP 异常,返回标准化的错误响应格式,并将错误信息记录到 Winston 日志文件中。
完整实现
第一步:创建过滤器
typescript
// src/filters/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
// 构造响应
const errorResponse = {
code: status,
timestamp: new Date().toISOString(),
message: exception.message || exception.name,
};
// 记录错误日志(同时输出到控制台和文件)
this.logger.error(
`${request.method} ${request.url} - ${errorResponse.message}`,
exception.stack,
);
// 返回响应
response.status(status).json(errorResponse);
}
}第二步:注册全局过滤器
typescript
// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();第三步:在业务代码中抛出异常
typescript
// user.controller.ts
import {
Controller,
Get,
Param,
NotFoundException,
ForbiddenException,
} from '@nestjs/common';
@Controller('user')
export class UserController {
@Get(':id')
async getUser(@Param('id') id: string) {
const user = null; // 模拟用户不存在
if (!user) {
throw new NotFoundException(`用户 ${id} 不存在`);
}
return user;
}
@Get('admin/users')
getAdminUsers() {
const currentUser = { isAdmin: false };
if (!currentUser.isAdmin) {
throw new ForbiddenException('User is not admin, forbidden to access');
}
return { users: [] };
}
}第四步:测试
bash
# 测试 404
curl http://localhost:3000/user/999
# → {"code":404,"timestamp":"...","message":"用户 999 不存在"}
# 测试 403
curl http://localhost:3000/user/admin/users
# → {"code":403,"timestamp":"...","message":"User is not admin, forbidden to access"}
# 测试未知路由
curl http://localhost:3000/unknown-path
# → {"code":404,"timestamp":"...","message":"Cannot GET /unknown-path"}常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 全局过滤器不生效 | 未在 main.ts 中注册 | app.useGlobalFilters(new XxxFilter()) |
| 注入 Logger 报错 | 过滤器未通过 DI 创建实例 | 使用 app.get(LOGGER_TOKEN) 获取实例 |
| 非 HTTP 异常未被捕获 | @Catch(HttpException) 只捕获 HTTP 异常 | 使用 @Catch() 不传参数捕获所有异常 |
| 多个全局过滤器冲突 | 全局 Filter 只能有一个 | 合并多个过滤器的逻辑到一个类中 |
| exception.stack 为空 | 异常被框架处理后丢失堆栈 | 在 catch 中尽早捕获并记录 |
学习要点总结
- 异常过滤器三层作用域:路由 → 控制器 → 全局,执行顺序由小到大
- 内置异常类:
NotFoundException、UnauthorizedException等,语义化、状态码自动匹配 - 自定义过滤器三步走:
@Catch()指定类型 →implements ExceptionFilter→ 实现catch()方法 - ArgumentsHost:统一接口获取 HTTP / RPC / WebSocket 上下文,通过
switchToHttp()获取 req/res - 全局过滤器 + 日志:在
main.ts中注册,自动记录所有异常到日志文件,无需手动 try-catch
延伸学习资源
官方文档
后续课程预告
- 守卫(Guards):认证与授权
- 拦截器(Interceptors):请求/响应转换与日志记录
- 管道(Pipes):数据验证与转换
课后作业
创建一个 WebSocket 异常过滤器(
WsExceptionFilter),捕获 WebSocket 连接中的异常,并通过client.emit('error', ...)返回错误信息给客户端。