debug一直是开发者的心头之痛,时常有人吐槽“20%的时间在写代码,而80%的时间都在调试代码”,哪怕项目发布后,一个线上问题也可能需要花很多时间去排查。那么我们如何在开发阶段多花点精力从而减少线上风险以及后续debug的时间投入呢? 显而易见,增加程序的鲁棒性的方法之一便是的正确的错误处理,下面我将总结下在nodeJs应用中应该如何进行合理的错误处理。

nodeJs异常抛出

与其他语言(Java, C++)不一样的情况是,nodeJs大部分错误都会出现在异步函数中,同步函数中很少需要抛出异常。

一般来说,常见的异步函数的异常抛出方法大概有四种:

  • 普通回调:
callback(new Error('something error'), null);
  • promise:
reject(new Error('something error')); 
  • throw:
throw new Error('something error')
  • EventEmitter
myEmitter.emit('error', new Error('something error'));

同步函数的错误抛出则一般是在JSON.parse的时候以及一些验证用户输入的情况。

错误类型

一般来说,错误类型可以分为两种:操作错误(Operational errors),程序错误(Programmer errors)

  • 操作错误:这类错误一般跟你的程序本身无关,主要分为两种:一种是用户输入(或环境)问题,还有一种便是依赖外部服务问题(如数据库连接超时,依赖服务请求失败等)

  • 程序错误:这种一般来说便是程序bug, 比如没有验证用户输入,变量名使用错误等。

当然在某些情况下,没有被正确处理的操作错误就是程序错误。 举个简单的栗子,比如数据库连接失败了,这种属于操作错误,如果你没有对数据库连接失败的结果进行任何处理,那么这就是程序错误了,所以无论是操作错误还是程序错误,我们都需要认真对待,并对可能出现的错误进行合理的处理。

操作错误基本的处理方式有:

  • 直接处理错误
  • 将错误直接传递给client
  • 设置阈值的重试
  • log记录

最佳实践

  • 使用promise对异常进行catch

哪怕你的promise的链路过长,最深处的throw也能在最外层catch住:

doWork()
 .then(doWork)
 .then(doOtherWork)
 .then((result) => doWork)
 .catch((error) => throw error)

但如果你不幸忘记了catch(比如一些promise和generater混用的时候),那么该nodeJs进程会触发unhandledRejection这个事件,并结束进程。这个情况在node 6.x的早期版本(以及古老的4.x)是不会主动有任何异常提示,错误就这样被生吞了,很容易导致后续调试困难。所以,为了避免这种情况发生,最好对unhandledRejection这个事件进行监听,记录相应的错误信息。哪怕你忘记处理promise异常也不会导致在node低版本中吞掉错误。如:

process.on('unhandledRejection', function (error, p) {
  errlog();
  throw error;
});
  • 处理未捕获的异常

在某些场景下,有些异常可能会没有捕获到。可以通过监听uncaughtException事件来进行相应的处理

process.on('uncaughtException', function (error) {
  //I just received an error that was never handled, time to handle it and then decide whether a restart is needed
  errorManagement.handler.handleError(error);
  if (!errorManagement.handler.isTrustedError(error))
    process.exit(1);
});
  • 类型检查

有一部分操作错误是来源于用户的输入错误,如果没有及早对用户输入进行类型判断和验证,并抛出异常,那么你的程序也是存在问题的。所以为了合理避免由于输入错误导致其他异常,应该严格按照你的文档规范对输入的变量进行类型检查,并根据你对输入的要求抛出正确的错误类型和错误信息。

TypeErrorRangeError的例子:

  if (typeof val !== 'number') {
    throw new TypeError(`${val} is not a number`);
  }

  if (val > max || val < min) {
    throw new RangeError(`${val} is not between ${min} and ${max}`);
  }
  • 操作错误和程序错误分别处理。

如果你的应用重度依赖第三方服务,务必将操作错误和程序错误分开处理。

//marking an error object as operational 
var myError = new Error("How can I add new product when no value provided?");
myError.isOperational = true;
 
//or if you're using some centralized error factory (see other examples at the bullet "Use only the built-in Error object")
function appError(commonType, description, isOperational) {
    Error.call(this);
    Error.captureStackTrace(this);
    this.commonType = commonType;
    this.description = description;
    this.isOperational = isOperational;
};
 
throw new appError(errorManagement.commonErrors.InvalidInput, "Describe here what happened", true);
 
//error handling code within middleware
process.on('uncaughtException', function(error) {
    if(!error.isOperational)
        process.exit(1);
});

  • 错误集中处理, 但不要在中间件上处理。

为了防止错误多次重复处理或者错误处理不当,错误处理最好能集中在一个对象上,但不要写在中间件上,因为中间件不能处理非Web接口的错误。

//handling errors within a dedicated object
module.exports.handler = new errorHandler();
 
function errorHandler(){
    this.handleError = function (error) {
        return logger.logError(err).then(sendMailToAdminIfCritical).then(saveInOpsQueueIfCritical).then(determineIfOperationalError);
    }
}

总结

提高nodeJs鲁棒性,完善的单测覆盖和合理的错误处理都必不可少。虽然会增加不少开发成本,但对于后续调试、重构以及维护有着不可或缺的作用。

参考资料:

https://www.joyent.com/node-js/production/design/errors

http://goldbergyoni.com/checklist-best-practices-of-node-js-error-handling/