Skip to content
第 182 / 250 章Node⏱ 12 分钟阅读

第 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 种

动手练习

  1. 创建一个 BusinessException 业务异常
  2. 写一个全局 ExceptionFilter 统一响应格式
  3. 在 Filter 中记录错误日志到文件
  4. 实现 TypeORM QueryFailedError 转 409

推荐阅读


下一章:第 183 章:配置管理(ConfigModule)

本站基于 VitePress 构建 · 由 Codebook 团队维护