异常过滤器
闯关 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),你应该优先使用它或它的子类。
基础用法:
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;
}
你甚至可以传递一个对象作为第一个参数,来覆盖整个响应体:
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: '这是一个自定义的错误消息',
}, HttpStatus.FORBIDDEN);
NestJS 还内置了很多继承自 HttpException
的常用异常类,让代码更具可读性:
BadRequestException
(400)UnauthorizedException
(401)NotFoundException
(404)ForbiddenException
(403)- ...等等
所以,上面的例子可以写得更优雅:
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
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,
});
}
}
让我们来分解一下这段代码:
@Catch(HttpException)
:这是一个装饰器,它告诉 NestJS,这个过滤器只关心类型为HttpException
(或其子类)的异常。如果留空@Catch()
,它会捕获所有的异常。catch(exception, host)
:这是ExceptionFilter
接口要求必须实现的方法。exception
: 当前正在处理的异常对象,也就是我们throw
出来的那个。host
:ArgumentsHost
是一个非常强大的工具类。简单来说,它是一个参数的包装器,可以帮助我们获取到原始的请求处理函数的参数,无论当前是 HTTP、WebSockets 还是 gRPC 上下文。在这里,我们用host.switchToHttp()
来明确获取 HTTP 上下文,并从中拿到request
和response
对象。
- 自定义响应:我们从异常对象中获取状态码 (
status
) 和消息 (message
),然后结合请求的 URL 和时间戳,构造了一个全新的、结构化的 JSON 对象,并将其发送回客户端。
4. 如何应用异常过滤器?
创建好过滤器后,我们需要告诉 NestJS 在哪里使用它。你有三种“作用域”可以选择:
方式一:方法作用域 (Method-scoped)
只对单个路由处理函数生效。
import { HttpExceptionFilter } from '../filters/http-exception.filter';
@Post()
@UseFilters(new HttpExceptionFilter()) // 在这里应用
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException('这是一条只会被 HttpExceptionFilter 捕获的消息');
}
方式二:控制器作用域 (Controller-scoped)
对一个控制器内的所有路由处理函数生效。
import { HttpExceptionFilter } from '../filters/http-exception.filter';
@Controller('cats')
@UseFilters(new HttpExceptionFilter()) // 在这里应用
export class CatsController {
// ... 这个控制器下的所有方法都会应用此过滤器
}
方式三:全局作用域 (Global-scoped)
对整个应用的所有路由生效。这是最常用的方式,可以确保整个应用的错误响应格式统一。
在 main.ts
文件中进行设置:
src/main.ts
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
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
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)。
现在,你已经掌握了处理应用“异常情况”的超能力了!