NestJS小技巧07-怎样抛出异常才能更专业

1,410 阅读5分钟
by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权

原文链接: blog.devops.dev

大家好我是雪隐,请叫我雪宝,欢迎来到NodeJS框架NestJS的第一个提示和技巧系列。我想分享一下我对如何为您的服务改进错误处理的想法。我们希望实现什么?

  • 轻松搜索和引用发生的错误
  • 丰富和分类服务错误
  • 将我们的错误作为度量标准公开

1. 引导项目

首先,像往常一样,让我们为我们引导一个示例服务。我建议您使用强大的nest-cli为我们的应用程序生成骨架:

npx nest new tip07
*# ...  
# ? Which package manager would you ❤️ to use?  
# > npm*

2. 注入错误

让我们直接抛出一些错误!在app.controller.ts中将代码更改为:

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    throw new Error('Oh no!');;
  }
}

是的,让我们从基础开始:首先抛出一个错误。我将使用Postman作为测试工具。让我们看看我们得到了什么回应:

{
    "statusCode": 500,
    "message": "Internal server error"
}

哇!我们甚至没有看到相应的错误消息。深入NestJS文档,您会发现一些绑定到HTTP响应状态的HTTP异常的良好层次结构。

3. 使用NestJS HttpExceptions

让我们使用@nestjs/common包中的HttpException

import { Controller, Get,HttpException,HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    throw new HttpException(  
      'Oh no!',  
      HttpStatus.INTERNAL_SERVER_ERROR  
      );
  }
}

测试一下:

{
    "statusCode": 500,
    "message": "Oh no!"
}

好吧,现在我们至少有一条错误消息。

还要注意,常见HTTP状态有异常别名,例如:InternalServerErrorExceptionBadRequestException等。在此处查找内置异常的完整列表。

如果你查看日志,你不会发现任何与错误有关的信息。如果它在生产中,那将是多么遗憾啊!让我们把它修好!

4. 日志记录错误

记录错误的过程在官方文档中有很好的描述。我们可以使用BaseExceptionFilter来捕获所有或特定的错误。

让我们创建一个新的文件exception.filter.ts

import { Catch, ArgumentsHost, Logger } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class CustomExceptionFilter extends BaseExceptionFilter {
  private readonly logger = new Logger(CustomExceptionFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    this.logger.error(JSON.stringify(exception));
    super.catch(exception, host);
  }
}

并告诉NestJS将其用作main.ts中的全局筛选器:

import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CustomExceptionFilter } from './exception/exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 我们稍后会处理掉
  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new CustomExceptionFilter(httpAdapter));
  await app.listen(3000);
}
bootstrap();

让我们试试看:

{
    "statusCode": 500,
    "message": "Oh no!"
}

查看日志:

[Nest] 1888  - 2023/03/17 19:27:51     LOG [NestApplication] Nest application successfully started +2ms
[Nest] 1888  - 2023/03/17 19:28:29   ERROR [CustomExceptionFilter] {"response":"Oh no!","status":500,"message":"Oh no!","name":"HttpException"}

对这正是我们想要的。现在,至少我们可以观察到我们的例外情况。

为什么我们在这种情况下没有使用ExceptionFilter接口?简单地说,因为在这一步中,我们将失去NestJS提供的序列化机制。进一步阅读如何制作带有丰富异常的自己的序列化程序。

但是,简单的错误消息并不能为我们提供所发生错误的任何细节。此外,它还不够“机器可读”。让我们来解决它。

5. 丰富异常

典型异常应该包含哪些额外的财产?就我个人而言,这至少是身份证、域名和时间戳。但是,如何在不失去NestJS的力量的情况下实现它呢?

首先,让我们为所需的异常business.exception.ts定义一个类:

import { HttpStatus } from '@nestjs/common';

export type ErrorDomain = 'users' | 'orders' | 'generic';

export class BusinessException extends Error {
  public readonly id: string;
  public readonly timestamp: Date;

  constructor(
    public readonly domain: ErrorDomain,
    public readonly message: string,
    public readonly apiMessage: string,
    public readonly status: HttpStatus,
  ) {
    super(message);
    this.id = BusinessException.genId();
    this.timestamp = new Date();
  }

  private static genId(length = 16): string {
    const p = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    return [...Array(length)].reduce(
      (a) => a + p[~~(Math.random() * p.length)],
      '',
    );
  }
}

让我们按属性进行细分:

  • id包含在初始化异常时生成的唯一标识符;
  • timestamp存储初始化异常时的时间戳;
  • domain指定此错误属于哪个业务域或发生错误的位置;
  • message包含用于日志记录的内部消息(可能包含私有数据,例如标识符、异常消息、堆栈跟踪等);
  • apiMessage包含要在对用户的响应中返回的消息。这个是公开曝光的;
  • status指定发生此错误时服务必须响应的HTTP状态。

6. 使用丰富的异常

为了集成这些异常,我们需要两件事:实际开始使用它们,并调整ExceptionFilter以正确处理这些错误。

  1. 让我们在app.controller.ts中抛出自定义错误,而不是内置异常:
import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';
import { BusinessException } from './business.exception';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    const userId = 1;
    throw new BusinessException(
      'users', // Error domain
      `User with id=${userId} was not found.`, // Internal message
      'User not found', // API message
      HttpStatus.NOT_FOUND, // HTTP status
    );
  }
}

  1. 调整我们的ExceptionFilter以正确处理我们的自定义异常,在exception.filter.ts中将BaseExceptionFilter替换为ExceptionFilter
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { BusinessException, ErrorDomain } from '../business.exception';

export interface ApiError {
  id: string;
  domain: ErrorDomain;
  message: string;
  timestamp: Date;
}

@Catch(Error)
export class CustomExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(CustomExceptionFilter.name);

  catch(exception: Error, host: ArgumentsHost) {
    let body: ApiError;
    let status: HttpStatus;

    if (exception instanceof BusinessException) {
      // 直接处理我们自己的异常
      body = {
        id: exception.id,
        message: exception.apiMessage,
        domain: exception.domain,
        timestamp: exception.timestamp,
      };
      status = exception.status;
    } else if (exception instanceof HttpException) {
      // 我们可以从NestJS错误中提取内部消息和状态对类验证器(class-validator)有用
      body = new BusinessException(
        'generic',
        exception.message,
        exception.message, // 如果你喜欢,也可以选择普通消息
        exception.getStatus(),
      );
      status = exception.getStatus();
    } else {
      // 对于所有其他异常,只需返回500错误
      body = new BusinessException(
        'generic',
        `Internal error occurred: ${exception.message}`,
        'Internal error occurred',
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
      status = HttpStatus.INTERNAL_SERVER_ERROR;
    }

    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    // 日志将包含错误标识符以及发生错误的请求路径
    this.logger.error(
      `Got an exception: ${JSON.stringify({
        path: request.url,
        ...body,
      })}`,
    );

    response.status(status).json(body);
  }
}

main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CustomExceptionFilter } from './exception/exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 无需在此处使用变通方法,因为我们正在使用
  // ExceptionFilter而不是BaseExceptionFilterr;
  app.useGlobalFilters(new CustomExceptionFilter());
  await app.listen(3000);
}

bootstrap();

让我们测试一下!

{
    "id": "fRaKKy8IBKf4v2K1",
    "message": "User not found",
    "domain": "users",
    "timestamp": "2023-03-17T11:35:54.628Z"
}

查看日志:

[Nest] 2118  - 2023/03/17 19:35:54   ERROR [CustomExceptionFilter] Got an exception: {"path":"/","id":"fRaKKy8IBKf4v2K1","message":"User not found","domain":"users","timestamp":"2023-03-17T11:35:54.628Z"}

不错!

总结

在本文中,我简要地(实际上不是)描述了如何在几行代码中丰富和记录NestJS错误。下一个技巧部分描述如何使用Prometheus将您的错误暴露为度量,以提高您的服务可观察性。

本章代码

代码