nestjs:如何优雅的处理异常

645 阅读4分钟

对于错误的处理,是一个后台开发人员必须熟练掌握的,今天来学习下nestjs如何处理错误。

当访问一个不存在的路由时,nestjs会返回一个如下的错误格式:

image.png

当一个接口内部存在逻辑错误时,nestjs会返回一个如下的错误格式:

@Get()
  findAll() {
    // 报错
    const a: any = null;
    return a.b.c;
  }

image.png

这些都是 Exception Filter 做的事情。它是在 Nest 应用抛异常的时候,捕获它并返回一个对应的响应。

但是,这些响应的格式不符合我们的要求,怎么办呢?

那就需要我们自定义异常时返回的响应格式了。

自定义exception filter

首先利用脚手架创建一个exception filter:

nest g filter father --flat --no-spec

// father.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';

@Catch()
export class FatherFilter<T> implements ExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {}
}

然后在main.ts中引入:

app.useGlobalFilters(new FatherFilter());

当然,如果你想局部启用,可以加在 handler 或者 controller 上:

@Controller('father')
@UseFilters(FatherFilter)
export class FatherController {}

@Get()
@UseFilters(FatherFilter)
findAll() {}

接下来进一步完善内部逻辑,@Catch 指定要捕获的异常,这里指定 BadRequestException

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common';

import { Response } from 'express';

@Catch(BadRequestException)
export class FatherFilter implements ExceptionFilter {

  catch(exception: BadRequestException, host: ArgumentsHost) {
    const http = host.switchToHttp();
    const response = http.getResponse<Response>();
    
    const statusCode = exception.getStatus();

    response.status(statusCode).json({
      code: statusCode,
      message: exception.message,
      extra: {},
      success:false
    });
  }
}

通过下面的代码抛出异常,这个异常就会被FatherFilter捕获,进入到catch函数逻辑里面,从而返回我们自定义的格式。

@Get()
  findAll() {
    throw new BadRequestException('xxxxx');
    return this.fatherService.findAll();
 }

但我们只是 @Catch 了 BadRequestException

如果抛的是其他异常,依然是原来的格式,比如我抛出throw new BadGatewayException('xxxxx');,我们自定义的exception filter并没有被捕获到这个错误。

其实,我们只要 @Catch 指定 HttpException 就行了。因为 BadRequestExeptionBadGateWayException 等都是它的子类。

@Catch(HttpException)
export class FatherFilter implements ExceptionFilter {

  catch(exception: HttpException, host: ArgumentsHost) {
    const http = host.switchToHttp();
    const response = http.getResponse<Response>();
    
    const statusCode = exception.getStatus();

    response.status(statusCode).json({
      code: statusCode,
      message: exception.message,
      extra: {},
      success:false
    });
  }
}

通过上面的了解,我们知道nest是如何捕获错误的,但是这远远不够,下面我将讲解在实际项目中如何进行异常的捕获。

实际项目异常处理示例

一般,我们分别要处理统一异常与 HTTP 类型的接口相关异常。

统一异常你可以理解为一个兜底的错误处理。示例代码如下:

import { FastifyReply, FastifyRequest } from "fastify";

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpStatus,
  ServiceUnavailableException,
  HttpException,
} from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: Error, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<FastifyReply>();
    const request = ctx.getRequest<FastifyRequest>();

    request.log.error(exception)

    // 非 HTTP 标准异常的处理。
    response.status(HttpStatus.SERVICE_UNAVAILABLE).send({
      statusCode: HttpStatus.SERVICE_UNAVAILABLE,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: new ServiceUnavailableException().getResponse(),
    });
  }
}

可以看到,Catch 的参数为空时,默认捕获所有异常。new ServiceUnavailableException()表示该接口服务不可用,作为兜底处理。

处理HTTP 类型的接口相关异常时, Catch 的参数为 HttpException 将只捕获 HTTP 相关的异常错误。

import { FastifyReply, FastifyRequest } from "fastify";
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
} from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<FastifyReply>();
    const request = ctx.getRequest<FastifyRequest>();
    const status = exception.getStatus();

    response.status(status).send({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.getResponse(),
    });
  }
}

定义好这两个exception filter文件后,在 main.ts 文件中添加 useGlobalFilters 全局过滤器:

app.useGlobalFilters(new AllExceptionsFilter(), new HttpExceptionFilter());

这里一定要注意引入自定义异常的先后顺序,不然异常捕获逻辑会出现混乱。

验证一下,当访问一个不存在的接口:

image.png

验证完 HTTP 异常之后,我们接着在 UserController 中伪造一个程序运行异常的接口,来验证常规异常是否能被正常捕获:

@Get('findError')
findError() {
    const a: any = {};
    console.log(a.b.c);
    return this.userService.findAll();
}

image.png

除了全局异常拦截处理之外,我们需要再新建一个 business.exception.ts 来处理业务运行中预知且主动抛出的异常

首先,我们自定义一个Exception:

import { HttpException, HttpStatus } from '@nestjs/common';
import { BUSINESS_ERROR_CODE } from './business.error.codes';

type BusinessError = {
  code: number;
  message: string;
};

export class BusinessException extends HttpException {
  constructor(err: BusinessError | string) {
    if (typeof err === 'string') {
      err = {
        code: BUSINESS_ERROR_CODE.COMMON,
        message: err,
      };
    }
    super(err, HttpStatus.OK);
  }

  static throwForbidden() {
    throw new BusinessException({
      code: BUSINESS_ERROR_CODE.ACCESS_FORBIDDEN,
      message: '抱歉哦,您无此权限!',
    });
  }
}

然后再定义一些错误码:

export const BUSINESS_ERROR_CODE = {
  // 公共错误码
  COMMON: 10001,
  // 特殊错误码
  TOKEN_INVALID: 10002,
  // 禁止访问
  ACCESS_FORBIDDEN: 10003,
  // 权限已禁用
  PERMISSION_DISABLED: 10003,
  // 用户已冻结
  USER_DISABLED: 10004,
};

简单改造一下 HttpExceptionFilter,在处理 HTTP 异常返回之前先处理业务异常:

import { FastifyReply, FastifyRequest } from "fastify";
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { BusinessException } from "./business.exception";

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<FastifyReply>();
    const request = ctx.getRequest<FastifyRequest>();
    const status = exception.getStatus();

    // 处理业务异常
    if (exception instanceof BusinessException) {
      const error = exception.getResponse();
      response.status(HttpStatus.OK).send({
        data: null,
        status: error['code'],
        extra: {},
        message: error['message'],
        success: false,
      });
      return;
    }

    response.status(status).send({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.getResponse(),
    });
  }
}

完成配置之后,我们继续在 UserController 中重新伪造一个业务异常的场景:

@Get('findBusinessError')
findBusinessError() {
    const a: any = {};
    try {
      console.log(a.b.c);
    } catch (error) {
      throw new BusinessException('你这个参数错了');
    }
    return this.userService.findAll();
}

image.png

总结

nestjs处理了三类错误:

  1. 业务错误:这个是我们主动抛出的错误,通过自定义一个exception以及特殊的错误码来完成,这类错误主要是方便我们排查业务。

  2. HTTP类错误:主要是前端访问404,访问无权限,前端参数错误等错误。

  3. 代码逻辑错误:这类错误主要是程序员写的代码有问题,一般不会主动抛出,因此需要程序有一个兜底的捕获机制,防止进程挂掉。