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