对于错误的处理,是一个后台开发人员必须熟练掌握的,今天来学习下nestjs如何处理错误。
当访问一个不存在的路由时,nestjs会返回一个如下的错误格式:
当一个接口内部存在逻辑错误时,nestjs会返回一个如下的错误格式:
@Get()
findAll() {
// 报错
const a: any = null;
return a.b.c;
}
这些都是 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 就行了。因为 BadRequestExeption、BadGateWayException 等都是它的子类。
@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());
这里一定要注意引入自定义异常的先后顺序,不然异常捕获逻辑会出现混乱。
验证一下,当访问一个不存在的接口:
验证完 HTTP 异常之后,我们接着在 UserController 中伪造一个程序运行异常的接口,来验证常规异常是否能被正常捕获:
@Get('findError')
findError() {
const a: any = {};
console.log(a.b.c);
return this.userService.findAll();
}
除了全局异常拦截处理之外,我们需要再新建一个 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();
}
注意:这里一定要区分自定义exception filter 和自定义exception的区别。
自定义 Exception:只负责定义异常,告诉程序 “发生了什么类型的错误、错误的状态码 / 提示语”,是数据层。
// 自定义异常:手机号格式错误
export class PhoneException extends HttpException {
constructor(message: string = '手机号格式错误') {
// 父类参数:提示语、HTTP状态码
super(message, HttpStatus.BAD_REQUEST);
}
}
@Controller('users')
export class UserController {
@Get('by-phone')
getUserByPhone(@Query('phone') phone: string) {
// 业务判断,抛出「自定义异常」
if (!/^1[345789]\d{9}$/.test(phone)) {
throw new PhoneException(`手机号 ${phone} 格式错误`); // 抛自定义异常
}
return { code: 200, msg: '成功' };
}
}、
自定义 Exception Filter:只负责处理异常,捕获所有异常,按规则格式化响应、记录日志,是处理层。
它们是配合关系,先定义不同类型的异常(贴标签),再用过滤器统一处理(按标签做不同响应),缺一不可。
总结
nestjs处理了三类错误:
-
业务错误:这个是我们主动抛出的错误,通过自定义一个
exception以及特殊的错误码来完成,这类错误主要是方便我们排查业务。 -
HTTP类错误:主要是前端访问404,访问无权限,前端参数错误等错误。
-
代码逻辑错误:这类错误主要是程序员写的代码有问题,一般不会主动抛出,因此需要程序有一个兜底的捕获机制,防止进程挂掉。