第 182 章:异常过滤器(Exception Filter)
学习目标
- 理解异常处理机制
- 掌握内置 HttpException
- 学会自定义 ExceptionFilter
- 实现统一异常响应
一、内置异常
1.1 HttpException 基类
typescript
import { HttpException, HttpStatus } from '@nestjs/common';
// 方式一
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
// 方式二
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
}, 403);1.2 常用子类
typescript
import {
BadRequestException, // 400
UnauthorizedException, // 401
ForbiddenException, // 403
NotFoundException, // 404
ConflictException, // 409
UnprocessableEntityException, // 422
InternalServerErrorException, // 500
NotImplementedException, // 501
BadGatewayException, // 502
ServiceUnavailableException, // 503
} from '@nestjs/common';
throw new NotFoundException(`User ${id} 不存在`);
// 默认响应
// { "statusCode": 404, "message": "User 1 不存在" }1.3 自定义消息
typescript
throw new BadRequestException({
statusCode: 400,
message: '邮箱格式不正确',
error: 'Validation Error',
details: ['邮箱不能为空', '邮箱必须包含 @'],
});二、自定义异常类
typescript
// exceptions/business.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
export class BusinessException extends HttpException {
constructor(message: string, code = 1000) {
super(
{
code, // 业务码
message,
timestamp: Date.now(),
},
HttpStatus.OK, // HTTP 状态码还是 200,业务状态看 code
);
}
}
export class AuthFailedException extends HttpException {
constructor(message = '认证失败') {
super({ code: 1001, message }, HttpStatus.UNAUTHORIZED);
}
}
export class PermissionDeniedException extends HttpException {
constructor(message = '权限不足') {
super({ code: 1002, message }, HttpStatus.FORBIDDEN);
}
}
// 使用
throw new BusinessException('库存不足', 2001);三、异常过滤器
3.1 实现 ExceptionFilter
typescript
// filters/all-exception.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch() // 捕获所有异常
export class AllExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.getResponse()
: 'Internal server error';
this.logger.error(
`${request.method} ${request.url}`,
exception instanceof Error ? exception.stack : String(exception),
);
response.status(status).json({
code: status,
message: typeof message === 'string' ? message : (message as any).message,
data: null,
timestamp: Date.now(),
path: request.url,
});
}
}3.2 注册
typescript
// 方式一:方法级
@Post()
@UseFilters(new AllExceptionFilter())
create(@Body() dto: CreateUserDto) {}
// 方式二:控制器级
@Controller('users')
@UseFilters(AllExceptionFilter)
export class UserController {}
// 方式三:全局
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new AllExceptionFilter());
await app.listen(3000);
}
// 方式四(推荐):全局 + DI
@Module({
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionFilter,
},
],
})
export class AppModule {}四、按异常类型捕获
4.1 单一异常
typescript
import { Catch, HttpException, ArgumentsHost } from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();
response.status(status).json({
code: status,
message: exception.message,
timestamp: Date.now(),
});
}
}4.2 多个异常
typescript
@Catch(BadRequestException, NotFoundException)
export class ValidationFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
// 只处理这俩
}
}4.3 自定义业务异常
typescript
@Catch(BusinessException)
export class BusinessExceptionFilter implements ExceptionFilter {
catch(exception: BusinessException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const body = exception.getResponse() as any;
response.status(200).json({
code: body.code,
message: body.message,
data: null,
timestamp: body.timestamp,
});
}
}五、统一异常响应格式
typescript
interface ApiError {
code: number;
message: string;
data: null;
timestamp: number;
path: string;
errors?: any[];
}
@Catch()
export class UnifiedExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
let body: ApiError = {
code: 500,
message: 'Internal server error',
data: null,
timestamp: Date.now(),
path: request.url,
};
if (exception instanceof HttpException) {
const status = exception.getStatus();
const res = exception.getResponse();
body = {
...body,
code: status,
message: typeof res === 'string'
? res
: (res as any).message,
errors: typeof res === 'object' ? (res as any).errors : undefined,
};
response.status(status);
} else if (exception instanceof Error) {
// 未捕获错误
Logger.error(exception.message, exception.stack, 'UnifiedFilter');
response.status(500);
}
response.json(body);
}
}六、TypeORM 异常处理
typescript
import { QueryFailedError } from 'typeorm';
@Catch(QueryFailedError)
export class DatabaseExceptionFilter implements ExceptionFilter {
catch(exception: QueryFailedError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const driverError = exception.driverError;
let status = 500;
let message = '数据库错误';
// MySQL 唯一约束冲突
if (driverError.code === 'ER_DUP_ENTRY') {
status = 409;
message = '记录已存在';
}
response.status(status).json({ code: status, message, data: null });
}
}七、记录日志
typescript
@Catch()
export class LoggingExceptionFilter implements ExceptionFilter {
private logger = new Logger('Exception');
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest();
const response = ctx.getResponse();
const status = exception instanceof HttpException
? exception.getStatus()
: 500;
// 结构化日志
this.logger.error({
path: request.url,
method: request.method,
status,
ip: request.ip,
user: request.user,
body: request.body,
query: request.query,
params: request.params,
message: exception instanceof Error ? exception.message : String(exception),
stack: exception instanceof Error ? exception.stack : undefined,
});
response.status(status).json({
code: status,
message: 'Server error',
});
}
}八、WebSocket 异常
typescript
@Catch()
export class WsExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const client = host.switchToWs().getClient();
const data = host.switchToWs().getData();
client.emit('exception', {
message: exception instanceof Error ? exception.message : 'Error',
data,
});
}
}九、RPC 异常
typescript
@Catch()
export class RpcExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
return throwError(() => exception);
}
}十、异常处理优先级
优先级
方法级 > 控制器级 > 全局级 > 默认
十一、本章小结
| 概念 | 关键 |
|---|---|
| HttpException | 异常基类 |
| 内置异常 | 400 / 401 / 403 / 404 / 409 |
| 自定义异常 | 继承 HttpException |
| @Catch() | 标记过滤器 |
| ArgumentsHost | 上下文访问 |
| 注册方式 | 4 种 |
动手练习
- 创建一个 BusinessException 业务异常
- 写一个全局 ExceptionFilter 统一响应格式
- 在 Filter 中记录错误日志到文件
- 实现 TypeORM QueryFailedError 转 409