Node.js最佳实践exception处理

我刚刚在几天前开始尝试node.js。 我已经意识到,只要我的程序中有一个未处理的exception,Node就会被终止。 这与我已经暴露的正常的服务器容器不同,在那里只有Worker Thread在发生未处理的exception并且容器仍然能够接收请求时死亡。 这提出了几个问题:

  • process.on('uncaughtException')是防范它的唯一有效方法吗?
  • process.on('uncaughtException')会在执行asynchronous进程期间捕获未处理的exception呢?
  • 是否有一个已经构build的模块(如发送电子邮件或写入文件),我可以利用未捕获的exception的情况下?

我将不胜感激任何指针/文章,会告诉我处理node.js中的未捕获exception的最佳实践

更新:Joyent现在在这个答案中提到了自己的指导 。 以下信息是更多的总结:

安全地“抛出”错误

理想情况下,我们希望尽可能避免未被捕获的错误,而不是直接抛出错误,而是根据我们的代码体系结构,使用以下方法之一安全地“抛出”错误:

  • 对于同步代码,如果发生错误,则返回错误:

     // Define divider as a syncrhonous function var divideSync = function(x,y) { // if error condition? if ( y === 0 ) { // "throw" the error safely by returning it return new Error("Can't divide by zero") } else { // no error occured, continue on return x/y } } // Divide 4/2 var result = divideSync(4,2) // did an error occur? if ( result instanceof Error ) { // handle the error safely console.log('4/2=err', result) } else { // no error occured, continue on console.log('4/2='+result) } // Divide 4/0 result = divideSync(4,0) // did an error occur? if ( result instanceof Error ) { // handle the error safely console.log('4/0=err', result) } else { // no error occured, continue on console.log('4/0='+result) } 
  • 对于基于callback(即asynchronous)的代码,callback的第一个参数是err ,如果发生错误,那么err是错误,如果没有发生错误,则errnull 。 任何其他参数遵循err参数:

     var divide = function(x,y,next) { // if error condition? if ( y === 0 ) { // "throw" the error safely by calling the completion callback // with the first argument being the error next(new Error("Can't divide by zero")) } else { // no error occured, continue on next(null, x/y) } } divide(4,2,function(err,result){ // did an error occur? if ( err ) { // handle the error safely console.log('4/2=err', err) } else { // no error occured, continue on console.log('4/2='+result) } }) divide(4,0,function(err,result){ // did an error occur? if ( err ) { // handle the error safely console.log('4/0=err', err) } else { // no error occured, continue on console.log('4/0='+result) } }) 
  • 对于事件性的代码,错误可能发生在任何地方,而不是抛出错误,而不是引发error事件 :

     // Definite our Divider Event Emitter var events = require('events') var Divider = function(){ events.EventEmitter.call(this) } require('util').inherits(Divider, events.EventEmitter) // Add the divide function Divider.prototype.divide = function(x,y){ // if error condition? if ( y === 0 ) { // "throw" the error safely by emitting it var err = new Error("Can't divide by zero") this.emit('error', err) } else { // no error occured, continue on this.emit('divided', x, y, x/y) } // Chain return this; } // Create our divider and listen for errors var divider = new Divider() divider.on('error', function(err){ // handle the error safely console.log(err) }) divider.on('divided', function(x,y,result){ console.log(x+'/'+y+'='+result) }) // Divide divider.divide(4,2).divide(4,0) 

安全地“捕捉”错误

有时候,如果我们没有安全地捕捉到它,可能仍然会有代码在某个地方抛出一个错误,导致一个未被捕获的exception和一个潜在的应用程序崩溃。 根据我们的代码体系结构,我们可以使用以下方法之一来捕捉它:

  • 当我们知道错误发生的位置时,我们可以将该节包装在一个node.js域中

     var d = require('domain').create() d.on('error', function(err){ // handle the error safely console.log(err) }) // catch the uncaught errors in this asynchronous or synchronous code block d.run(function(){ // the asynchronous or synchronous code that we want to catch thrown errors on var err = new Error('example') throw err }) 
  • 如果我们知道错误发生的地方是同步代码,并且出于任何原因不能使用域(可能是旧版本的节点),我们可以使用try catch语句:

     // catch the uncaught errors in this synchronous code block // try catch statements only work on synchronous code try { // the synchronous code that we want to catch thrown errors on var err = new Error('example') throw err } catch (err) { // handle the error safely console.log(err) } 

    但是,请注意不要在asynchronous代码中使用try...catch ,因为asynchronous抛出的错误不会被捕获:

     try { setTimeout(function(){ var err = new Error('example') throw err }, 1000) } catch (err) { // Example error won't be caught here... crashing our app // hence the need for domains } 

    另一件要小心try...catch的风险是在try语句中包含完成callback的风险,如下所示:

     var divide = function(x,y,next) { // if error condition? if ( y === 0 ) { // "throw" the error safely by calling the completion callback // with the first argument being the error next(new Error("Can't divide by zero")) } else { // no error occured, continue on next(null, x/y) } } var continueElsewhere = function(err, result){ throw new Error('elsewhere has failed') } try { divide(4, 2, continueElsewhere) // ^ the execution of divide, and the execution of // continueElsewhere will be inside the try statement } catch (err) { console.log(err.stack) // ^ will output the "unexpected" result of: elsewhere has failed } 

    随着代码变得越来越复杂,这个问题很容易做到。 因此,最好是使用域或返回错误,以避免(1)asynchronous代码中未捕获的exception(2)您不希望的尝试捕获执行。 在允许正确的线程代替JavaScript的asynchronous事件机器风格的语言中,这不是一个问题。

  • 最后,如果在未包含在域或try catch语句中的地方发生未捕获的错误,我们可以通过使用uncaughtException监听器来使应用程序不会崩溃(但是这样做可能会使应用程序处于未知状态状态 ):

     // catch the uncaught errors that weren't wrapped in a domain or try catch statement // do not use this in modules, but only in applications, as otherwise we could have multiple of these bound process.on('uncaughtException', function(err) { // handle the error safely console.log(err) }) // the asynchronous or synchronous code that emits the otherwise uncaught error var err = new Error('example') throw err 

以下是来自许多不同来源的关于此主题的摘要和策展,包括来自所选博客post的代码示例和引用。 最佳实践的完整列表可以在这里find


Node.JSerror handling的最佳实践


Number1:使用承诺进行asynchronouserror handling

TL; DR:处理callback样式中的asynchronous错误可能是最糟糕的方式(也就是厄运的金字塔)。 你可以给你的代码最好的礼物是使用一个信誉良好的承诺库,它提供了非常紧凑和熟悉的代码语法,如try-catch

否则: Node.JScallback风格,函数(错误,响应),是由于error handling与临时代码混合,过度嵌套和笨拙的编码模式而导致的不可维护代码的有希望的方式

代码示例 – 很好

 doWork() .then(doWork) .then(doError) .then(doWork) .catch(errorHandler) .then(verify); 

代码示例反模式 – callback风格error handling

 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)

“…实际上,callback的做法更加邪恶:他们剥夺了我们通常认为的编程语言的堆栈,没有堆栈编写代码就像驾驶没有刹车踏板的汽车一样:没有意识到你需要多less东西,直到你接近它,而不是在那里,承诺的全部重点是让我们回到asynchronous时我们失去的语言基础:返回,抛出和堆栈。必须知道如何正确地使用承诺,以利用它们。


Number2:只使用内置的Error对象

TL; DR:查看将错误作为string或自定义types抛出的代码很常见 – 这使error handling逻辑和模块之间的互操作性变得复杂。 无论您是否拒绝承诺,抛出exception或发出错误 – 使用Node.JS内置的Error对象可以提高均匀性并防止错误信息丢失

否则:在执行某个模块时,不确定哪种types的错误会返回 – 使得很难推断即将发生的exception并处理它。 即使值得使用自定义types来描述错误,也可能会导致重要的错误信息(如堆栈跟踪)的丢失!

代码示例 – 正确

  //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?"); 

博客引用:“一个string不是一个错误” (从博客devthought,关键字“Node.JS错误对象”排名6)

“…传递string而不是错误导致模块之间的互操作性降低,它违反了可能正在执行instanceof错误检查的API,或者想知道更多关于错误的信息 。除了把消息传递给构造函数之外,现代JavaScript引擎中有趣的属性..“


编号3:区分运行和程序员错误

TL; DR:操作错误(例如,API收到无效input)指的是已知的错误影响已被充分理解的情况,可以慎重处理。 另一方面,程序员错误(例如尝试读取未定义的variables)是指未知的代码失败,指示正常地重新启动应用程序

否则:当出现错误时,您总是可以重新启动应用程序,但是为什么~5000个在线用户因为次要和预测错误(操作错误)而closures? 相反也不是理想的 – 当未知问题(程序员错误)发生时保持应用程序可能导致不可预测的行为。 区分这两者可以在适当的环境下巧妙地采取平衡的方法

代码示例 – 正确

  //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); }); 

博客引用 :“否则你冒险的状态”(从博客可debugging,排名3为关键字“Node.JS未捕获的exception”)

……由于JavaScript的投掷方式的本质,几乎没有任何方法可以安全地”捡起你离开的地方“,而不会泄露引用,或者创build其他types的不确定的脆弱状态。抛出的错误是closures进程 ,当然,在一个普通的Web服务器上,可能会有很多连接打开,由于其他人触发错误而突然closures这些连接是不合理的,更好的方法是向触发错误的请求发送错误响应,同时让其他人在正常时间内完成,并停止监听该工作人员的新请求“


Number4:集中处理错误,但不在中间件内

TL; DR:error handling逻辑,如邮件pipe理和日志logging,应封装在一个专用的集中对象,所有的端点(如快递中间件,cron作业,unit testing)在出现错误时调用。

否则:在一个地方不处理错误将导致代码重复,并可能导致error handling不当

代码示例 – 一个典型的错误stream程

 //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.JSerror handling”排名1)

“…你可能最终会在堆栈的几个层次上处理相同的错误,这种情况发生在较低级别除了将错误传播给调用者,并将错误传播给调用者等情况之外,只有顶级调用者知道什么是适当的响应,无论是重试操作,向用户报告错误还是别的,但这并不意味着你应该把所有错误报告给一个顶层callback,因为callback本身不知道在什么情况下发生错误“


Number5:使用SwaggerloggingAPI错误

TL; DR:让你的API调用者知道哪些错误可能会回来,以便他们能够处理这些问题而不会崩溃。 这通常使用Swagger等REST API文档框架完成

否则: API客户端可能会决定崩溃并重新启动,因为他收到了他无法理解的错误。 注意:您的API的调用者可能是您(在微服务环境中非常典型)

博客引用: “你必须告诉你的调用者可能发生什么错误”(来自Joyent博客,关键字“Node.JS logging”排名第一)

…我们已经讨论了如何处理错误,但是当你正在编写一个新的函数时,你如何向调用函数的代码传递错误? …如果你不知道会发生什么错误或者不知道他们的意思,那么你的程序就不会是正确的,除非是意外。 所以如果你正在写一个新的函数,你必须告诉你的调用者什么错误可以发生,他们是什么


Number6:当一个陌生人来到城里时,优雅地closures这个过程

TL; DR:发生未知错误(开发人员错误,请参阅最佳实践编号#3) – 应用程序的健康性存在不确定性。 通常的做法是build议使用像Forever和PM2这样的“重新启动”工具来重新启动这个过程

否则:当一个不熟悉的exception被捕获时,某个对象可能处于错误状态(例如,由于某些内部故障,全局使用的事件发射器不再触发事件),并且所有未来的请求可能会失败或performanceexception

代码示例 – 决定是否崩溃

 //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; } 

博客引用: “关于error handling有三种想法”(来自jsrecipes博客)

…关于error handling主要有三种想法:1.让应用程序崩溃并重新启动它。 2.处理所有可能的错误,永远不会崩溃。 3.两者之间的平衡办法


Number7:使用成熟的logging器来增加错误可见性

TL; DR:像Winston,Bunyan或者Log4J这样的成熟的日志工具,会加速错误的发现和理解。 所以忘了console.log。

否则:通过console.logs浏览或手动通过凌乱的文本文件,而不查询工具或体面的日志查看器可能会让你忙于工作,直到晚

代码示例 – Winstonlogging器正在运行

 //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' }); 

博客引用: “让我们识别一些需求(对于一个logging器):”(来自博客的strongblog)

…让我们识别一些要求(对于logging器):1.时间戳每个日志行。 这个很自我解释 – 你应该能够知道每个日志条目何时发生。 2.logging格式应该容易被人类和机器消化。 3.允许多个可configuration的目标stream。 例如,您可能正在将跟踪日志写入一个文件,但遇到错误时,请写入相同的文件,然后写入错误文件并同时发送电子邮件。


Number8:使用APM产品发现错误和停机时间

TL; DR:监控和性能产品(又名APM)主动评估您的代码库或API,以便它们可以自动奇迹般突出显示错误,崩溃并放慢您缺less的部分

否则:您可能会花很多精力测量API性能和停机时间,可能您永远不会意识到哪些是您在现实世界场景下最慢的代码部分,以及它们如何影响UX

博客引用: “APM产品细分”(来自Yoni Goldberg博客)

“… APM产品构成了三个主要部分: 1.网站或API监控 –通过HTTP请求持续监控正常运行时间和性能的外部服务,可以在几分钟内完成安装,以下是一些竞争者:Pingdom,Uptime Robot和New Relic 2代码仪器 –需要在应用程序中embedded代理以获益的代码仪器 –产品系列特性包括慢代码检测,exception统计,性能监控等等以下是select的竞争者:New Relic,App Dynamics 3.操作智能仪表板 –这些行的产品都专注于为操作团队提供指标和策划的内容,这有助于轻松地保持应用程序的性能,这通常涉及到多个信息源(应用程序日志,数据库日志,服务器日志等)和前期的仪表板devise以下是几个select的竞争者:Datadog,Splunk“


以上是一个缩短的版本 – 在这里看到更多的最佳做法和例子

你可以捕获未捕获的exception,但是使用有限。 参见http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions:4c933d54-1428-443c-928d-4e1ecbdd56cb

monitforeverupstart可以用来重新启动节点进程,当它崩溃。 正常关机最好是你可以希望的(例如将所有内存数据保存在未捕获的exception处理程序中)。

nodejs域是处理nodejs中错误的最新方法。 域可以捕获错误/其他事件以及传统抛出的对象。 域也提供了处理callback的function,通过拦截方法将错误作为第一个parameter passing。

与正常的try / catch式error handling一样,通常最好是在发生错误时抛出错误,并且阻止要隔离错误的区域影响其余的代码。 “屏蔽”这些区域的方法是用一个函数调用domain.run作为一个独立的代码块。

在同步代码中,上面的代码就足够了 – 当发生错误时,要么让它被抛出,要么抓住它并在那里处理,还原所有需要还原的数据。

 try { //something } catch(e) { // handle data reversion // probably log too } 

当asynchronouscallback中发生错误时,您需要能够完全处理数据的回滚(共享状态,数据库等外部数据)。 或者你必须设置一些东西来表明发生了exception – 你在哪里关心该标志,你必须等待callback完成。

 var err = null; var d = require('domain').create(); d.on('error', function(e) { err = e; // any additional error handling } d.run(function() { Fiber(function() { // do stuff var future = somethingAsynchronous(); // more stuff future.wait(); // here we care about the error if(err != null) { // handle data reversion // probably log too } })}); 

上面的一些代码很难看,但是你可以为自己创build一些模式,使它更漂亮,例如:

 var specialDomain = specialDomain(function() { // do stuff var future = somethingAsynchronous(); // more stuff future.wait(); // here we care about the error if(specialDomain.error()) { // handle data reversion // probably log too } }, function() { // "catch" // any additional error handling }); 

更新(2013-09):

上面,我使用了一个暗示纤维语义的未来,它允许你在线等待期货。 这实际上允许你使用传统的try-catch块来做所有的事情 – 我认为这是最好的方法。 但是,你不能总是这样做(即在浏览器中)…

也有期货不需要纤维语义(然后与普通的浏览JavaScript一起工作)。 这些可以被称为期货,承诺,或延期(我只是指从这里的期货)。 简单的旧的JavaScript期货库允许在期货之间传播错误。 只有其中一些库允许任何抛出的未来被正确处理,所以要小心。

一个例子:

 returnsAFuture().then(function() { console.log('1') return doSomething() // also returns a future }).then(function() { console.log('2') throw Error("oops an error was thrown") }).then(function() { console.log('3') }).catch(function(exception) { console.log('handler') // handle the exception }).done() 

这模仿了正常的try-catch,即使这些部分是asynchronous的。 它会打印:

 1 2 handler 

请注意,它不会打印“3”,因为抛出的exception会中断该stream。

看看蓝鸟的承诺:

请注意,我还没有发现许多其他库,这些正确处理抛出的exception。 jQuery的推迟,例如,不 – “失败”处理程序将永远不会得到exception抛出一个“然后”处理程序,在我看来这是一个交易断路器。

我最近在http://snmaynard.com/2012/12/21/node-error-handling/上写了这个。; 在0.8版本中,节点的一个新function是域,并允许将所有forms的error handling组合成一个更简单的pipe理forms。 你可以在我的文章中阅读关于他们。

您也可以使用Bugsnag之类的东西来跟踪您的未捕获的exception,并通过电子邮件,聊天室通知或有一个未捕获的exception(我是Bugsnag的联合创始人)创build一张票。

在使用forEach循环时,使用try-catch的一个例子可能是合适的。 它是同步的,但同时你不能在内部范围内使用return语句。 相反,可以使用try和catch方法来返回适当范围内的Error对象。 考虑:

 function processArray() { try { [1, 2, 3].forEach(function() { throw new Error('exception'); }); } catch (e) { return e; } } 

这是上述@balupton描述的方法的组合。

我只想补充一点, Step.js库通过总是将它传递给下一个步骤函数来帮助你处理exception。 因此,您可以将最后一步作为一个函数来检查上述任何步骤中的任何错误。 这种方法可以大大简化您的error handling。

以下是来自github页面的引用:

任何引发的exception都会被捕获并作为下一个函数的第一个parameter passing。 只要你不嵌套callback函数内联你的主要function,这可以防止任何未捕获的exception。 这对长时间运行的node.JS服务器非常重要,因为单个未捕获的exception可能会导致整个服务器停机。

此外,您可以使用Step来控制脚本的执行,从而将清理部分作为最后一步。 例如,如果您想要在Node中编写构build脚本,并报告需要多长时间才能写入,最后一步可以做到这一点(而不是试图挖掘最后一次callback)。

After reading this post some time ago I was wondering if it was safe to use domains for exception handling on an api / function level. I wanted to use them to simplify exception handling code in each async function I wrote. My concern was that using a new domain for each function would introduce significant overhead. My homework seems to indicate that there is minimal overhead and that performance is actually better with domains than with try catch in some situations.

http://www.lighthouselogic.com/#/using-a-new-domain-for-each-async-function-in-node/

If you want use Services in Ubuntu(Upstart): Node as a service in Ubuntu 11.04 with upstart, monit and forever.js

Catching errors has been very well discussed here, but it's worth remembering to log the errors out somewhere so you can view them and fix stuff up.

​Bunyan is a popular logging framework for NodeJS – it supporst writing out to a bunch of different output places which makes it useful for local debugging, as long as you avoid console.log. ​ In your domain's error handler you could spit the error out to a log file.

 var log = bunyan.createLogger({ name: 'myapp', streams: [ { level: 'error', path: '/var/tmp/myapp-error.log' // log ERROR to this file } ] }); 

This can get time consuming if you have lots of errors and/or servers to check, so it could be worth looking into a tool like Raygun (disclaimer, I work at Raygun) to group errors together – or use them both together. ​ If you decided to use Raygun as a tool, it's pretty easy to setup too

 var raygunClient = new raygun.Client().init({ apiKey: 'your API key' }); raygunClient.send(theError); 

​ Crossed with using a tool like PM2 or forever, your app should be able to crash, log out what happened and reboot without any major issues.