You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NestJS如何结构化代码优雅处理应用错误?

NestJS 错误处理最佳实践(消除重复try/catch样板)

核心原则(先解决你的选型疑问)

  • 异常抛出遵循谁发现问题谁抛出:Service层检测到业务规则不满足(比如用户不存在、密码错误)就直接抛语义化异常,不需要传到Resolver/Controller层再抛;Controller/Resolver只负责参数校验、调用Service、返回响应,不重复实现业务错误判断。
  • 通用错误逻辑(日志打印、错误格式化、未知错误转500响应)统一交给全局异常过滤器实现,全应用只需要写一次,完全不需要在每个业务方法里重复写try/catch,符合DRY原则。拦截器适合做请求前后的通用切面逻辑(比如响应序列化、性能打点),不适合做错误兜底处理。
  • fn().then().catch() 和 async/await + try/catch 只是Promise的不同写法,本质没有区别,换写法不会减少样板代码。统一用async/await可读性更高,配合全局过滤器绝大多数场景不需要写catch块。
  • 只有当你需要对特定错误做降级处理(比如调用第三方接口失败返回默认值)时,才需要在业务代码里写局部try/catch,否则不要手动捕获异常。

你当前代码的问题

  1. 每个方法都包try/catch实现日志打印+抛500错误,属于完全可抽离的重复样板代码。
  2. catch块逻辑存在bug:你主动抛出的400状态码业务异常,会被catch块捕获,直接覆盖成500服务端错误,前端无法拿到正确的错误提示和状态码。

注意:你当前的catch块是"吞错"逻辑,会丢失原始错误的语义,线上排查问题时很难定位具体错误原因。

重构实现

第一步:精简业务方法,移除冗余try/catch

业务逻辑里只保留分支判断和对应异常抛出,不需要手动捕获错误:

async validateUser(
  email: string,
  plainTextPassword: string,
): Promise<User | null> {
  const user = await this.usersRepository.findOne({ email });
  if (!user) {
    throw new BadRequestException("Wrong credentials provided");
  }

  const isMatch = await this.verifyPassword(
    plainTextPassword,
    user.password,
  );
  if (!isMatch) {
    throw new BadRequestException("Wrong credentials provided");
  }
  await this.usersRepository.filter(user);
  return user;
}

async verifyPassword(plainTextPassword: string, hashedPassword: string) {
  return bcrypt.compare(plainTextPassword, hashedPassword);
}

注:上面用的BadRequestException是Nest内置的标准异常类,等价于new HttpException(msg, HttpStatus.BAD_REQUEST),写法更简洁。Nest内置了BadRequestException、UnauthorizedException、NotFoundException、InternalServerErrorException等常用异常类,可直接使用。

第二步:实现全局异常过滤器统一兜底

全局异常过滤器会自动捕获整个应用所有未被手动处理的异常,在这里统一实现日志打印、错误格式化、未知错误转500响应的逻辑,全应用生效:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Response, Request } from 'express';

@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>();

    // 区分主动抛出的HTTP异常和未知运行时错误
    const isHttpException = exception instanceof HttpException;
    const httpStatus = isHttpException 
      ? exception.getStatus() 
      : HttpStatus.INTERNAL_SERVER_ERROR;
    const responseMsg = isHttpException
      ? exception.getResponse()
      : "Something went wrong";

    // 统一打印错误日志,不需要每个业务方法单独写
    this.logger.error(
      `[请求错误] 路径:${request.url} 方法:${request.method} 信息:${exception instanceof Error ? exception.message : exception}`,
      exception instanceof Error ? exception.stack : ''
    );

    // 统一返回标准化错误响应
    response.status(httpStatus).json({
      statusCode: httpStatus,
      message: responseMsg,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

第三步:注册全局过滤器

在应用入口文件main.ts中注册过滤器,即可对所有接口生效:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 注册全局异常过滤器
  app.useGlobalFilters(new AllExceptionsFilter());
  await app.listen(3000);
}
bootstrap();

额外优化建议

  • 如果是GraphQL场景,全局异常过滤器需要适配GraphQL的ExecutionContext,逻辑和HTTP场景一致,都是统一捕获异常后返回格式化的错误信息。
  • 对于数据库操作、第三方服务调用产生的未知运行时错误,不需要手动捕获,全局过滤器会自动记录日志并返回500响应。
  • 局部try/catch捕获异常后,如果不需要做降级处理,一定要把异常重新抛出,不要吞错,否则全局过滤器无法感知到错误,不会打印日志也不会返回正确的错误响应。

内容的提问来源于stack exchange,提问作者wongx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 04:42:40