以下是该主题的许多不同来源的摘要和精选,包括代码示例和部分博客文章的引文。最佳做法的完整列表可以在这里找到
Node.JS错误处理的最佳实践
编号1:使用诺言进行异步错误处理
TL; DR:以回调方式处理异步错误可能是通向地狱的最快方法(又名“厄运金字塔”)。您可以给代码提供的最好礼物是使用信誉良好的Promise库,该库提供了很多紧凑和熟悉的代码语法,例如try-catch
否则:由于错误处理与临时代码,过多的嵌套和笨拙的编码模式相结合,Node.JS回调样式,函数(错误,响应)是一种无法维护代码的有前途的方法
代码示例-好
doWork()
.then(doWork)
.then(doError)
.then(doWork)
.catch(errorHandler)
.then(verify);
代码示例反模式–回调样式错误处理
getData(someParameter, function(err, result){
if(err != null)
//do something like calling the given callback function and pass the error
getMoreData(a, function(err, result){
if(err != null)
//do something like calling the given callback function and pass the error
getMoreData(b, function(c){
getMoreData(d, function(e){
...
});
});
});
});
});
博客
语录:“我们在承诺方面有问题”(来自博客pouchdb,关键字“节点承诺”排名11)
“……事实上,回调的作用更加险恶:它们剥夺了我们的堆栈,这在编程语言中通常是我们所理所当然的。没有堆栈的代码编写就像在没有刹车的情况下驾驶汽车:直到达到并没有达到目标时,才意识到自己有多急。诺言的全部目的是让我们恢复异步时丢失的语言基础:返回,抛出和堆栈。必须知道如何正确使用诺言才能利用它们。 ”
2号:仅使用内置的Error对象
TL; DR:将代码以字符串或自定义类型抛出错误的代码很常见–这使错误处理逻辑和模块之间的互操作性变得复杂。无论您拒绝承诺,引发异常还是发出错误-使用Node.JS内置的Error对象,可以提高一致性并防止错误信息丢失
否则:在执行某个模块时,由于不确定返回的错误类型–使得推理和处理异常变得更加困难。甚至值得,使用自定义类型来描述错误可能会导致诸如堆栈跟踪之类的关键错误信息丢失!
代码示例-正确执行
//throwing an Error from typical function, whether sync or async
if(!productToAdd)
throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promise
return new promise(function (resolve, reject) {
DAL.getProduct(productToAdd.id).then((existingProduct) =>{
if(existingProduct != null)
return reject(new Error("Why fooling us and trying to add an existing product?"));
代码示例反模式
//throwing a String lacks any stack trace information and other important properties
if(!productToAdd)
throw ("How can I add new product when no value provided?");
博客引用:“字符串不是错误”
(来自博客devthought,关键字“ Node.JS error object”的排名为6)
“…传递字符串而不是错误会导致模块之间的互操作性降低。它破坏了与可能正在执行错误检查实例或想要了解更多有关错误的API的契约。正如我们将看到的,错误对象具有除了保留传递给构造函数的消息外,现代JavaScript引擎还具有有趣的特性。”
3号:区分操作错误与程序员错误
TL; DR:操作错误(例如,API接收到无效输入)是指可以充分理解并可以深思熟虑地处理错误影响的已知情况。另一方面,程序员错误(例如,尝试读取未定义的变量)是指未知的代码错误,这些错误指示必须正常重启应用程序
否则:您可能总是在出现错误时重新启动应用程序,但是为什么由于次要和预期的错误(操作错误)而导致约5000个在线用户失望?相反也不是理想的选择-在发生未知问题(程序员错误)时保持应用程序正常运行可能会导致意外行为。区分两者允许机智地采取行动,并根据给定的上下文应用平衡的方法
代码示例-正确执行
//throwing an Error from typical function, whether sync or async
if(!productToAdd)
throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promise
return new promise(function (resolve, reject) {
DAL.getProduct(productToAdd.id).then((existingProduct) =>{
if(existingProduct != null)
return reject(new Error("Why fooling us and trying to add an existing product?"));
代码示例-将错误标记为可操作(受信任)
//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);
});
博客语录:“否则,您就要冒状态的风险”(可调试博客中,关键字“ Node.JS未捕获的异常”排名3)
“ …从本质上讲,throw在JavaScript中是如何工作的,几乎没有任何方法可以安全地“从中断的地方开始”,而不会泄漏引用或创建其他未定义的易碎状态。响应的最安全方法抛出的错误是关闭的过程。当然,在一个正常的Web服务器,你可能有很多连接打开,因为错误是由其他人触发时,它是不合理的突然关闭这些了。更好的方法是向触发错误的请求发送错误响应,同时让其他请求在正常时间内完成操作,并停止监听该工作进程中的新请求”
第四条:通过中间件而不是中间件集中处理错误
TL; DR:错误处理逻辑(例如发给管理员的邮件和日志记录)应封装在专用的集中对象中,当出现错误时,所有端点(例如Express中间件,cron作业,单元测试)都将调用该对象。
否则:不在一个地方处理错误将导致代码重复,并可能导致错误处理错误
代码示例-典型错误流
//DAL layer, we don't handle errors here
DB.addDocument(newCustomer, (error, result) => {
if (error)
throw new Error("Great error explanation comes here", other useful parameters)
});
//API route code, we catch both sync and async errors and forward to the middleware
try {
customerService.addNew(req.body).then(function (result) {
res.status(200).json(result);
}).catch((error) => {
next(error)
});
}
catch (error) {
next(error);
}
//Error handling middleware, we delegate the handling to the centrzlied error handler
app.use(function (err, req, res, next) {
errorHandler.handleError(err).then((isOperationalError) => {
if (!isOperationalError)
next(err);
});
});
博客引用: “有时较低的级别除了将错误传播给调用者之外,无济于事”(在博客Joyent中,关键字“ Node.JS错误处理”排名1)
“……您可能最终会在堆栈的多个级别上处理相同的错误。当较低的级别除了将错误传播给调用者,将错误传播给其调用者之外,无法执行任何其他操作时,就会发生这种情况。通常,只有顶层调用者知道什么是适当的响应,无论是重试操作,向用户报告错误还是其他,但这并不意味着您应该尝试将所有错误报告给单个顶层回调,因为该回调本身无法知道在什么情况下发生了错误”
5:使用Swagger记录文档API错误
TL; DR:让您的API调用者知道哪些错误可能会返回,以便他们可以认真处理这些错误而不会崩溃。这通常是通过REST API文档框架(例如Swagger)完成的
否则: API客户端可能决定崩溃并重新启动,仅是因为他收到了无法理解的错误。注意:API的调用者可能是您(在微服务环境中非常典型)
博客引用: “您必须告诉调用者可能发生什么错误”(来自Joyent博客,关键字“ Node.JS logging”排名1)
…我们已经讨论了如何处理错误,但是当您编写新函数时,如何将错误传递给调用函数的代码?…如果您不知道会发生什么错误或不知道错误的含义,那么您的程序除非是偶然的,否则是不正确的。因此,如果您要编写新函数,则必须告诉调用者可能发生的错误以及错误的含义。
6号:当一个陌生人来到小镇时,优雅地关闭该过程
TL; DR:当发生未知错误(开发人员错误,请参阅最佳实践编号3)时-应用程序的健康状况不确定。通常的做法是建议使用Forever和PM2等“重新启动器”工具仔细重新启动该过程
否则:当捕获到一个陌生的异常时,某些对象可能处于故障状态(例如,全局使用的事件发射器,并且由于某些内部故障而不再触发事件),并且所有将来的请求都可能失败或疯狂
代码示例-确定是否崩溃
//deciding whether to crash when an uncaught exception arrives
//Assuming developers mark known operational errors with error.isOperational=true, read best practice #3
process.on('uncaughtException', function(error) {
errorManagement.handler.handleError(error);
if(!errorManagement.handler.isTrustedError(error))
process.exit(1)
});
//centralized error handler encapsulates error-handling related logic
function errorHandler(){
this.handleError = function (error) {
return logger.logError(err).then(sendMailToAdminIfCritical).then(saveInOpsQueueIfCritical).then(determineIfOperationalError);
}
this.isTrustedError = function(error)
{
return error.isOperational;
}
博客语录: “关于错误处理的三种流派”(来自博客jsrecipes)
…关于错误处理,主要有三种思路:1.让应用程序崩溃并重新启动它。2.处理所有可能的错误,永不崩溃。3.两者之间的平衡方法
7号:使用成熟的记录器来提高错误可见性
TL; DR:一套成熟的日志记录工具,例如Winston,Bunyan或Log4J,将加快错误发现和理解的速度。因此,请忘记console.log。
否则:浏览console.logs或手动浏览混乱的文本文件而不使用查询工具或不错的日志查看器,可能会使您忙于工作直到很晚
代码示例-运行中的Winston记录器
//your centralized logger object
var logger = new winston.Logger({
level: 'info',
transports: [
new (winston.transports.Console)(),
new (winston.transports.File)({ filename: 'somefile.log' })
]
});
//custom code somewhere using the logger
logger.log('info', 'Test Log Message with some parameter %s', 'some parameter', { anything: 'This is metadata' });
博客引用: “让我们确定一些要求(对于记录器):”(来自博客strongblog)
…让我们确定一些要求(对于记录器):1.在每条日志行上打上时间戳。这是很容易解释的-您应该能够分辨出每个日志条目何时发生。2.日志记录格式应易于人类和机器消化。3.允许多个可配置的目标流。例如,您可能将跟踪日志写入一个文件,但是遇到错误时,先写入同一文件,然后写入错误文件并同时发送电子邮件...
第八条:使用APM产品发现错误和停机时间
TL; DR:监视和性能产品(aka APM)主动评估您的代码库或API,以便它们可以自动突出显示您所缺少的错误,崩溃和缓慢的部分
否则:您可能会花费大量精力来衡量API性能和停机时间,可能永远不会知道在现实情况下,哪些是您最慢的代码部分,以及它们如何影响UX
博客引用: “ APM产品细分”(来自博客Yoni Goldberg)
“……APM产品包括3个主要部分:1.网站或API监视–外部服务通过HTTP请求不断监视正常运行时间和性能。可以在几分钟内设置。以下是一些竞争者:Pingdom,正常运行时间机器人和New Relic
2 。代码工具–需要在应用程序中嵌入代理才能受益的产品系列,其特点是缓慢的代码检测,异常统计信息,性能监控等,以下是一些选定的竞争者:New Relic,App Dynamics
3.运营情报仪表板–这些产品系列专注于为操作团队提供指标和精心设计的内容,以帮助轻松地保持应用程序性能的最高水平。这通常涉及汇总多种信息源(应用程序日志,数据库日志,服务器日志等)和前期仪表板设计工作。以下是一些选定的竞争者:Datadog,Splunk”
上面是简化版- 请在此处查看更多最佳做法和示例