几天前我刚开始试用node.js。我已经意识到,每当我的程序中出现未处理的异常时,节点就会终止。这与我所接触的正常服务器容器不同,在正常服务器容器中,当发生未处理的异常时,只有工作线程死亡,并且容器仍然能够接收请求。这引发了几个问题:

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

如果有任何指针/文章向我展示在node.js中处理未捕获异常的常见最佳实践,我将不胜感激


当前回答

我只想补充一点,Step.js库通过始终将其传递给下一步函数来帮助您处理异常。因此,作为最后一步,您可以使用一个函数来检查前面任何步骤中的任何错误。这种方法可以大大简化错误处理。

下面是github页面的引用:

抛出的任何异常都会被捕获并作为第一个参数传递给下一个函数。只要不内联嵌套回调函数你的主要功能,这可以防止任何未完成的任务例外情况。这对于长期运行的node.JS服务器非常重要因为一个未捕获的异常会导致整个服务器停机。

此外,您可以使用步骤来控制脚本的执行,以便将清理部分作为最后一步。例如,如果您想在Node中编写一个构建脚本并报告编写所需的时间,那么最后一步可以做到这一点(而不是试图挖掘最后一个回调)。

其他回答

前段时间读了这篇文章后,我想知道在api/函数级别上使用域进行异常处理是否安全。我想用它们来简化我编写的每个异步函数中的异常处理代码。我担心的是,为每个函数使用一个新域会引入大量开销。我的作业似乎表明开销最小,而且在某些情况下,域的性能实际上比try-catch要好。

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

您可以捕获未捕获的异常,但它的用处有限。看见http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions:4c933d54-1428-434c-928d-4e1ecbd56cb

monit、forever或upstart可用于在节点进程崩溃时重新启动它。您最希望的是一个优雅的关闭(例如,将所有内存中的数据保存在未捕获的异常处理程序中)。

nodejs域是处理nodejs中错误的最新方法。域可以捕获错误/其他事件以及传统抛出的对象。域还提供了处理回调的功能,其中错误通过intercept方法作为第一个参数传递。

与正常的try/catch风格的错误处理一样,通常最好在错误发生时抛出错误,并将您希望隔离错误的区域排除在外,以免影响代码的其余部分。“屏蔽”这些区域的方法是调用domain.run,将一个函数作为隔离代码块。

在同步代码中,以上内容就足够了——当发生错误时,要么让它被抛出,要么抓住它并在那里处理,还原需要还原的任何数据。

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

当错误发生在异步回调中时,您要么需要能够完全处理数据的回滚(共享状态、数据库等外部数据)。或者,您必须设置一些东西来指示发生了异常——如果您关心该标志,则必须等待回调完成。

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
  }

})});

上面的一些代码很难看,但您可以为自己创建模式以使其更漂亮,例如:

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,即使片段是异步的。它将打印:

1
2
handler

请注意,它不打印“3”,因为引发了中断该流的异常。

看看蓝鸟的承诺:

https://github.com/petkaantonov/bluebird

注意,除了这些库之外,我还没有找到其他许多库可以正确处理抛出的异常。jQuery的延迟,例如,不要-“fail”处理程序永远不会让“then”处理程序抛出异常,在我看来,这是一个交易破坏者。

更新:Joyent现在有了自己的指南。以下信息更为概括:

安全地“抛出”错误

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

对于同步代码,如果发生错误,则返回错误://将除法器定义为同步函数var divideSync=函数(x,y){//如果错误条件?如果(y===0){//通过返回错误安全地“抛出”错误return new Error(“不能被零除”)}其他{//未发生错误,继续返回x/y}}//除以4/2var结果=分频同步(4,2)//是否发生错误?if(错误的结果实例){//安全地处理错误console.log('4/2=err',结果)}其他{//未发生错误,继续console.log('4/2='+结果)}//除以4/0结果=分频同步(4,0)//是否发生错误?if(错误的结果实例){//安全地处理错误console.log('4/0=err',结果)}其他{//未发生错误,继续console.log('4/0='+result)}对于基于回调的(即异步)代码,回调的第一个参数是err,如果发生错误,则err为错误,如果没有发生错误,那么err为空。err参数后面有任何其他参数:var divide=函数(x,y,next){//如果错误条件?如果(y===0){//通过调用完成回调安全地“抛出”错误//第一个参数是错误next(新错误(“不能被零除”))}其他{//未发生错误,继续next(空,x/y)}}除法(4,2,函数(错误,结果){//是否发生错误?if(错误){//安全地处理错误console.log('4/2=err',err)}其他{//未发生错误,继续console.log('4/2='+结果)}})除法(4,0,函数(错误,结果){//是否发生错误?if(错误){//安全地处理错误console.log('4/0=err',err)}其他{//未发生错误,继续console.log('4/0='+result)}})对于事件代码,如果错误可能发生在任何地方,而不是抛出错误,则触发错误事件://确定我们的分频器事件发射器var events=require('事件')var Divider=函数(){events.EventEmitter.call(this)}require('util').inherits(除法器,events.EventEmitter)//添加除法函数除法器prototype.divide=函数(x,y){//如果错误条件?如果(y===0){//通过发出错误来安全地“抛出”错误var err=新错误(“不能被零除”)this.emit('error',err)}其他{//未发生错误,继续这个.发射('分割',x,y,x/y)}//链条返回此;}//创建除法器并侦听错误var除法器=新除法器()divider.on('error',函数(err){//安全地处理错误console.log(错误)})除法器.on(“除法”,函数(x,y,结果){console.log(x+'/'+y+'='+result)})//划分除法器。除法(4,2)。除法(4,0)

安全地“捕捉”错误

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

当我们知道错误发生的位置时,我们可以将该部分包装在node.js域中var d=require('domain').create()d.on('error',函数(err){//安全地处理错误console.log(错误)})//捕获此异步或同步代码块中未捕获的错误d.run(函数){//要捕获抛出错误的异步或同步代码var err=新错误('example')抛出错误})如果我们知道发生错误的地方是同步代码,并且由于任何原因不能使用域(可能是旧版本的节点),我们可以使用try-catch语句://捕获此同步代码块中未捕获的错误//try-catch语句仅适用于同步代码尝试{//我们想要捕获抛出错误的同步代码var err=新错误('example')抛出错误}捕获(错误){//安全地处理错误console.log(错误)}但是,请注意不要使用try。。。在异步代码中捕获,因为不会捕获异步抛出的错误:尝试{setTimeout(函数){var err=新错误('example')抛出错误}, 1000)}捕获(错误){//此处不会捕获示例错误。。。崩溃我们的应用程序//因此需要域}如果你真的想和…一起工作。。catch与异步代码结合使用,当运行Node 7.4或更高版本时,您可以在本机使用async/await来编写异步函数。还有一件事要小心尝试。。。catch是在try语句中包装完成回调的风险,如下所示:var divide=函数(x,y,next){//如果错误条件?如果(y===0){//通过调用完成回调安全地“抛出”错误//第一个参数是错误next(新错误(“不能被零除”))}其他{//未发生错误,继续next(空,x/y)}}var continueOther=函数(错误,结果){抛出新错误('elswhere has failed')}尝试{划分(4,2,继续其他地方)//^执行分割//continue其他地方将在try语句中}捕获(错误){console.log(错误堆栈)//^将输出“意外”结果:其他地方已失败}当您的代码变得更复杂时,这个gotcha非常容易做到。因此,最好使用域或返回错误,以避免(1)异步代码中未捕获的异常(2)您不希望执行的try-catch-catching执行。在允许正确线程而不是JavaScript异步事件机样式的语言中,这不是什么问题。最后,如果一个未捕获的错误发生在一个没有被包装在域或try-catch语句中的地方,我们可以使用uncaughtException侦听器使我们的应用程序不会崩溃(但是这样做会使应用程序处于未知状态)://捕获未包装在域或try-catch语句中的未捕获错误//不要在模块中使用,而只能在应用程序中使用,否则我们可能会绑定多个process.on('ncaughtException',函数(err){//安全地处理错误console.log(错误)})//发出未捕获错误的异步或同步代码var err=新错误('example')抛出错误

这里已经很好地讨论了捕获错误,但值得记住的是将错误记录在某个位置,以便您可以查看它们并修复它们。

​Bunyan是NodeJS的一个流行日志框架,它支持向一系列不同的输出位置进行输出,这使得它对本地调试非常有用,只要您避免console.log。​在域的错误处理程序中,可以将错误输出到日志文件中。

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

如果您有大量错误和/或服务器需要检查,这可能会很耗时,因此值得研究Raygun(免责声明,我在Raygun工作)这样的工具来将错误分组在一起,或者将两者同时使用。​如果您决定使用Raygun作为工具,那么设置也非常简单

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

​在使用PM2或永久性工具的情况下,你的应用程序应该能够崩溃、注销发生的事情并重新启动,而不会出现任何重大问题。