异常过滤器
一、NestJS 异常处理机制
NestJS 有一个内置的全局异常层:任何未被处理的异常都会被捕获,并自动返回合适的 HTTP 响应。
默认行为:
HttpException及其子类 → 自动提取状态码和消息- 其他异常(
Error、数据库错误等)→ 返回500 Internal Server Error
异常过滤器(ExceptionFilter)允许你完全接管这个处理过程。
二、内置 HTTP 异常类
NestJS 内置了完整的 HTTP 异常类层次:
typescript
// 基类
throw new HttpException('自定义消息', HttpStatus.BAD_REQUEST);
throw new HttpException({ code: 'USER_NOT_FOUND', message: '用户不存在' }, 404);
// 常用子类(自动设置状态码)
throw new BadRequestException('参数错误'); // 400
throw new UnauthorizedException('未登录或 Token 过期'); // 401
throw new ForbiddenException('没有权限'); // 403
throw new NotFoundException(`用户 #${id} 不存在`); // 404
throw new MethodNotAllowedException(); // 405
throw new NotAcceptableException(); // 406
throw new RequestTimeoutException('请求超时'); // 408
throw new ConflictException('邮箱已被注册'); // 409
throw new GoneException(); // 410
throw new PayloadTooLargeException(); // 413
throw new UnsupportedMediaTypeException(); // 415
throw new UnprocessableEntityException(); // 422
throw new TooManyRequestsException('请求过于频繁'); // 429
throw new InternalServerErrorException('服务器错误'); // 500
throw new NotImplementedException(); // 501
throw new BadGatewayException(); // 502
throw new ServiceUnavailableException(); // 503
throw new GatewayTimeoutException(); // 504传递结构化消息
typescript
// 传入对象时,整个对象作为 response body
throw new BadRequestException({
code: 'VALIDATION_FAILED',
message: '参数验证失败',
details: [
{ field: 'email', error: '邮箱格式不正确' },
{ field: 'age', error: '年龄必须大于 0' },
],
});三、自定义异常类
推荐: 在业务层抛出语义化的业务异常,而非直接抛出 HTTP 异常。这样 Service 层不依赖 HTTP 语义,更便于测试和复用。
typescript
// exceptions/business.exception.ts
export class BusinessException extends HttpException {
constructor(
private readonly code: string,
message: string,
status: number = HttpStatus.BAD_REQUEST,
) {
super({ code, message }, status);
}
getCode() { return this.code; }
}
// 具体业务异常
export class UserNotFoundException extends BusinessException {
constructor(id: number) {
super('USER_NOT_FOUND', `用户 #${id} 不存在`, HttpStatus.NOT_FOUND);
}
}
export class EmailAlreadyExistsException extends BusinessException {
constructor(email: string) {
super('EMAIL_EXISTS', `邮箱 ${email} 已被注册`, HttpStatus.CONFLICT);
}
}
// 在 Service 中抛出
async findOne(id: number): Promise<User> {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new UserNotFoundException(id);
return user;
}四、自定义异常过滤器
捕获特定异常
typescript
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
// 提取消息(可能是字符串或对象)
const message =
typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message;
response.status(status).json({
code: status,
message,
path: request.url,
method: request.method,
timestamp: new Date().toISOString(),
});
}
}捕获所有异常(生产推荐)
typescript
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = '服务器内部错误';
let code = 'INTERNAL_ERROR';
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
message =
typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message ?? message;
code = (exceptionResponse as any).code ?? `HTTP_${status}`;
}
// 生产环境:记录非 4xx 错误(服务器端问题)
if (status >= 500) {
this.logger.error(
`${request.method} ${request.url} → ${status}`,
exception instanceof Error ? exception.stack : String(exception),
);
}
response.status(status).json({
code,
message,
path: request.url,
timestamp: new Date().toISOString(),
// 开发环境可附加堆栈
...(process.env.NODE_ENV === 'development' && exception instanceof Error
? { stack: exception.stack }
: {}),
});
}
}五、ArgumentsHost:多传输层支持
ArgumentsHost 是请求上下文的抽象,支持 HTTP、WebSocket、RPC:
typescript
@Catch()
export class UniversalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const type = host.getType<'http' | 'ws' | 'rpc'>();
if (type === 'http') {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
// HTTP 处理
response.status(500).json({ message: 'Error' });
} else if (type === 'ws') {
const ctx = host.switchToWs();
const client = ctx.getClient<WebSocket>();
// WebSocket 处理
client.send(JSON.stringify({ event: 'error', message: 'Error' }));
} else if (type === 'rpc') {
const ctx = host.switchToRpc();
// gRPC/Microservice 处理
throw new RpcException({ message: 'Error', statusCode: 500 });
}
}
}六、过滤器的绑定层级
typescript
// 1. 路由级别(优先级最高)
@Get(':id')
@UseFilters(UserNotFoundFilter)
findOne(@Param('id') id: string) {}
// 2. 控制器级别
@UseFilters(HttpExceptionFilter)
@Controller('users')
export class UsersController {}
// 3. 全局(推荐方式:通过 APP_FILTER 可以注入依赖)
@Module({
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}
// 也可以在 main.ts 注册(但无法注入 NestJS Provider)
app.useGlobalFilters(new AllExceptionsFilter());多过滤器时的捕获顺序:
typescript
@UseFilters(FilterA, FilterB)
// 若 FilterA 的 @Catch(SomeError) 匹配,FilterA 处理
// 若 FilterA 不匹配,FilterB 尝试
// 全局过滤器最后兜底七、与 ValidationPipe 的配合
ValidationPipe 验证失败时抛出 BadRequestException,包含详细字段错误:
typescript
// ValidationPipe 默认抛出:
// BadRequestException {
// message: ['name must be longer than or equal to 2 characters', 'email must be an email'],
// error: 'Bad Request',
// statusCode: 400
// }
// 在异常过滤器中处理(提取字段级错误):
@Catch(BadRequestException)
export class ValidationExceptionFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const exceptionResponse = exception.getResponse() as any;
response.status(400).json({
code: 'VALIDATION_FAILED',
message: '请求参数验证失败',
errors: Array.isArray(exceptionResponse.message)
? exceptionResponse.message
: [exceptionResponse.message],
});
}
}八、异常过滤器 vs 拦截器:如何选择?
| 场景 | 推荐 |
|---|---|
| 统一格式化错误响应(HTTP 状态码、message) | ExceptionFilter |
| 记录错误日志(异常发生时) | ExceptionFilter |
| 将某类错误转换为另一类(数据库约束 → 业务异常) | Interceptor(catchError) |
将响应体从对象包装为 {code, data} 格式 | Interceptor(map) |
最佳实践:
- Interceptor 的
catchError做异常转换(第三方库错误 → NestJS HttpException) - ExceptionFilter 做异常呈现(格式化为统一 JSON 响应)
可运行 Demo:
practice/02-request-lifecycle— 全局异常过滤器统一响应格式
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
| 自定义异常过滤器不生效 | 未注册或注册顺序错误 | @UseFilters() 装饰器或 app.useGlobalFilters() 全局注册 |
HttpException 子类被过滤器捕获不到 | 泛型类型不匹配 | 用 @Catch(HttpException) 或 @Catch() 捕获所有 |
| 生产环境暴露堆栈信息 | 过滤器直接透传 error.stack | NODE_ENV !== 'production' 时才包含 stack |