如何在KoaJS中处理错误?
KoaJS是洋葱模型,请求进入和返回时,会“经过”2次中间件。所以可以直接在第一个中间件中,通过try-catch捕获服务的错误。
async function errHandlerMiddleware(ctx, next) {
logger.log({
logType: 'accessLog',
reqPath: ctx.request.path,
reqId: ctx.request.reqId
// ... 更多信息
})
try {
await next()
} catch (err) {
logger.error({
logType: 'errHandle',
reqId: ctx.request.reqId,
errStack: inspect(err.stack),
errMsg: err.message
})
ctx.res.status = 500
ctx.res.body = "server error"
}
}
配合process.on,可以监听未捕获的unhandledRejection
、uncaughtException
如何在Express中处理错误?
Experss不是洋葱模型,而是中间件进行「串行处理」,请求返回时,不会再“经过”前面的中间件。所以,要在中间件中,通过事件监听,来响应服务中的报错,并且进行打印:
function errHandlerMiddleware(req, res, next) {
res.on('finish', () => {
// ...
})
res.on('error', (err) => {
// ...
})
}
在这个点上,express 不如 koa 优雅。
配合process.on,可以监听未捕获的unhandledRejection
、uncaughtException
如何在NestJS中处理错误?
分类讨论错误类型
由于Nest基于Express 2次开发,并且加了Ioc,因此处理起来比较棘手。
按照错误类型,可以按照以下方法来处理:
- 未捕获的
unhandledRejection
、uncaughtException
,比如异步处理报错、第三方库错误:通过process.on监听 - NestJS内置错误
HttpException
:通过全局Filter来捕获 - 服务定义的标准错误,继承自
HttpException
:通过全局Filter来捕获 - 服务未定义的错误:通过全局Filter来捕获
- 第三方库的错误:通过全局Filter来捕获
- HTTP 库的错误
- 非 HTTP 错误
实现标准错误类
项目中不允许直接在代码中,直接throw new Errro(xxxx)
抛出错误。此类错误会被Filter识别为sysErr
系统错误。
所有的错误都要通过标准的方法makeErr
进行抛出。
抛出的错误继承自 HttpException
,新增了错误码code
属性。
整体实现如下:
import { HttpException, HttpStatus } from '@nestjs/common';
import { isString } from 'lodash';
interface IMakeErrOpts {
code: string;
message: string;
statusCode?: number;
stack?: string;
}
/**
* 错误基类
*/
export class BaseError extends HttpException {
/**
* 业务错误码
*/
public readonly code: string;
/**
* 错误基类
* @param code 业务错误码
* @param message 错误信息
* @param stack 错误堆栈
* @param statusCode HTTP错误码
*/
constructor(code, message, stack, statusCode) {
super(message, statusCode);
if (isString(stack)) {
// 默认使用父类的上下文堆栈
this.stack = stack;
}
this.code = code;
}
}
/**
* 生成标准错误
* @param opts
* @returns
*/
export function makeErr(opts: IMakeErrOpts) {
const {
code,
message,
stack,
statusCode = HttpStatus.INTERNAL_SERVER_ERROR,
} = opts;
return new BaseError(code, message, stack, statusCode);
}
实现错误捕获Filter
从上面看到,2-5都通过Filter来捕获。因此,要在Filter中区分处理不同类型报错。
- 内置错误和继承自内置错误的标准错误:返回业务的错误码,打印错误信息
- 未定义错误:开发阶段未发现、未处理成标准错误的异常。返回系统错误码,红体打印系统错误。
- 第三方库的错误:
- HTTP 库错误:error上一般都有status,识别status即可。如果status是4xx,那么处理和第一类一样;否则,和第二类一样
- 非 HTTP 错误:看是否提供事件监听或者try-catch捕获,不然就是和第二类一样,属于未捕获错误。
整体实现如下:
@Catch()
export class AllExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const res: ICtxResponse = ctx.getResponse();
const req: ICtxRequest = ctx.getRequest();
const isSysErr = !(
exception instanceof HttpException ||
(exception.status >= 400 && exception.status < 500)
); // 正常错误包括:nest内置错误、继承nest内置错误的标准错误、第三方http库的4xx系列错误
// 日志类型
const logType = isSysErr ? 'sysErr' : 'responseErr';
// HTTP 状态码
const status: number = isSysErr
? HttpStatus.INTERNAL_SERVER_ERROR
: exception.status || HttpStatus.BAD_REQUEST;
// 业务错误码。用户可根据错误码来细化前端交互
const errCode: string = isSysErr
? SERVER_ERR_CODE
: exception.code || BAD_REQUEST_CODE;
// 错误详情
const errMsg: string = exception?.response?.message
? exception.message + '. ' + exception?.response?.message
: exception.message;
// 额外打印错误信息
baseLog.print({
logLevel: isSysErr ? 'error' : 'warn',
reqId: req.reqId,
logType,
logTime: Date.now(),
errStatus: status,
errMsg,
errStack: exception.stack,
errCode,
});
// 用于 accessLog 日志打印
req.accessErr = {
status,
code: errCode,
message: errMsg,
};
// 返回给用户标准数据
res.set('X-Content-Type-Options', 'nosniff').status(200).json({
reqId: req.reqId,
code: errCode,
msg: errMsg,
});
}
}