nodejs错误处理,及自定义错误信息

教你在node中自定义错误码

Posted by Li Yucang on June 8, 2018

nodejs 错误处理,及自定义错误信息

最近在用 egg 搭建自己博客的时候,想让 egg 接口不管错误与否全部返回 200 状态码,再携带上必要的信息,这样能方便前端的处理。查阅 egg 文档,如下写到:

框架通过 onerror 插件提供了统一的错误处理机制。对一个请求的所有处理方法(Middleware、Controller、Service)中抛出的任何异常都会被它捕获,并自动根据请求想要获取的类型返回不同类型的错误(基于 Content Negotiation)。

onerror 插件在生产环境下貌似能判断请求是需求html或者JSON,来返回一个有错误信息的简单错误页(请求html且未配置错误页面)、errorPageUrl(请求html且有配置错误页面)、JSON 对象或对应的 JSONP 格式响应,不带详细的错误信息(请求JSON)。俗话说,纸上得来终觉浅,接下来我们在一个接口的controller上写下各种可能抛出的错误类型,并且不对错误进行捕获:

  1. 最常见的 js 语法错误:
JSON.parse('a js error');

返回结果:

Internal Server Error, real status: 500
  1. 接着使用 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

看来一般的错误对象具有messagename属性,而通过validation包装的错误对象多出了codeerrors属性。通过观察,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,导致错误不能被捕获,会使服务器挂掉,此时需使用eggctx.runInBackground(scope)方法:

// 下单后需要进行一次核对,且不阻塞当前请求
try {
    ctx.service.trade.check(request); // 异步
} catch (e) { // 不能捕获到Error
    logger.error(err)
}
需要写成:
ctx.runInBackground(async () => {
  // 这里面的异常都会统统被 Backgroud 捕获掉,并打印错误日志
  await ctx.service.trade.check(request);
});