nodejs 错误处理,及自定义错误信息
最近在用 egg 搭建自己博客的时候,想让 egg 接口不管错误与否全部返回 200 状态码,再携带上必要的信息,这样能方便前端的处理。查阅 egg 文档,如下写到:
框架通过 onerror 插件提供了统一的错误处理机制。对一个请求的所有处理方法(Middleware、Controller、Service)中抛出的任何异常都会被它捕获,并自动根据请求想要获取的类型返回不同类型的错误(基于 Content Negotiation)。
onerror 插件在生产环境下貌似能判断请求是需求html
或者JSON
,来返回一个有错误信息的简单错误页(请求html
且未配置错误页面)、errorPageUrl(请求html
且有配置错误页面)、JSON 对象或对应的 JSONP 格式响应,不带详细的错误信息(请求JSON
)。俗话说,纸上得来终觉浅,接下来我们在一个接口的controller
上写下各种可能抛出的错误类型,并且不对错误进行捕获:
- 最常见的 js 语法错误:
JSON.parse('a js error');
返回结果:
Internal Server Error, real status: 500
- 接着使用 validate 插件对参数进行验证,查看验证失败报错:
ctx.validate(aRuleObj, ctx.request.query);
验证不通过时,返回结果:
422 Unprocessable Entity
这种报错一是信息太少不利于错误定位,二是返回结构不一致不利于前端对错误信息进行判断,三是返回非 200 错误码对于前端不太友好。so,egg 自身的错误返回显然不能满足我们的需求,我们需要自定义我们需要的错误信息。
既然要自定义错误信息,我们就得先了解错误对象(new Error()
)的一些属性:
属性比较简单,我们通过以下代码直接打印各种报错时的错误对象:
try {
ctx.validate(testRule, req);
// or JSON.parse('asdsadas');
// throw new Error('a error');
} catch (e) {
logger.error(e);
}
通过将错误打印出来,观察错误对象具有的属性:
validation错误:
message: Validation Failed
errors:[ { message: 'should not be empty', code: 'invalid', field: 'val'
} ]
code:invalid_param
name: UnprocessableEntityError
js语法错误:
message:Unexpected token a in JSON at position 0
errors:undefined
code:undefined
name: SyntaxError
自己抛出的错误:
message: a error
errors: undefined
code: undefined
name: Error
看来一般的错误对象具有message
和name
属性,而通过validation
包装的错误对象多出了code
和errors
属性。通过观察,error
属性是用来说明那个字段未通过验证,已及相应的验证信息,而code
表式错误码。我们根据这些规则来自定义我们的错误信息,为避免和已有的状态码冲突,我们的内部code
从 13000 开始:
// config.default.js 定义错误码
config.errors = {
ERR_NEED_VAL: {
code: 13000,
msg: 'ERR_NEED_VAL',
},
ERR_VAL_NEED_INT: {
code: 13000,
msg: 'ERR_VAL_NEED_INT',
}, ... }
// context.js 定义错误处理函数
serverError(err = {}, data = {}) {
const { app } = this;
let msg = '';
let code = '';
if (err.msg) {
// 自定义错误
msg = err.msg;
code = err.code;
} else if (err.message === 'Validation Failed') {
// 参数验证错误
msg = JSON.stringify(err.errors);
code = 422;
} else {
// 未定义错误
msg = err.message;
code = 500;
}
if (app.isProdEnv) {
// 生产环境下不暴露具体错误信息
msg = 'server error';
}
const rsp = this.helper.formatRsp(code, data, msg);
this.logger.error(err);
// 打印错误日志
this.body = rsp;
this.status = 200;
},
// helper.js
throwError(error) {
// 抛出自定义错误
const e = new Error(error.msg);
e.code = error.code;
e.msg = error.msg;
throw e;
},
formatRsp(code = 0, data = {}, msg = '') {
const errCode = (() => {
if (code > 12000) {
return code;
}
if (code !== 0) {
return 12000 + code;
}
return 0;
})();
return {
// Mark this sever error code, as this value may be overwrite
code: errCode,
// This server error code starts with 12000
data,
msg,
};
},
在controller
中我们对可能报错的操作try catch
起来,并调用serverError
方法:
test() {
const { ctx, app, service, logger } = this;
const req = Object.assign({}, ctx.request.query, ctx.request.body);
const { val } = ctx.query;
const { ERR_NEED_VAL } = app.config.errors;
try {
ctx.validate(testRule, req);
if (!val) {
ctx.helper.throwError(ERR_NEED_VAL); // 使用自定义错误
}
const rsp = service.test.test(val);
logger.info(`the value is set to ${JSON.stringify(rsp)}`);
ctx.success(rsp);
} catch (err) {
ctx.serverError(err);
}
}
最后,我们可以添加一个错误处理中间件,对未捕获的漏网之错误进行最后的打捞:
// error_handler.js
module.exports = () => {
return async function errorHandler(ctx, next) {
try {
await next();
} catch (err) {
ctx.serverError(err);
}
};
};
最后的最后,在进行非阻塞操作时会跳出当前try catch
,导致错误不能被捕获,会使服务器挂掉,此时需使用egg
的ctx.runInBackground(scope)
方法:
// 下单后需要进行一次核对,且不阻塞当前请求
try {
ctx.service.trade.check(request); // 异步
} catch (e) { // 不能捕获到Error
logger.error(err)
}
需要写成:
ctx.runInBackground(async () => {
// 这里面的异常都会统统被 Backgroud 捕获掉,并打印错误日志
await ctx.service.trade.check(request);
});