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

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

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


当前回答

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


Node.JS错误处理的最佳实践


数字1:使用promise进行异步错误处理

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

博客引用:“字符串不是错误”(来自博客devthink,关键字“Node.JS错误对象”排名第6)

“…传递字符串而不是错误会降低模块之间的互操作性。它会破坏与API的合同,这些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)

“……就抛出在JavaScript中的工作原理而言,几乎没有任何方法可以安全地“从你停止的地方继续”,而不泄漏引用,或创建其他类型的未定义脆性状态。响应抛出错误的最安全方法是关闭进程。当然,在正常的web服务器中,您可能有许多连接处于打开状态,因为错误是由其他人触发的,所以突然关闭这些连接是不合理的。更好的方法是对触发错误的请求发送错误响应,同时让其他人在正常时间内完成,并停止侦听该工作人员中的新请求。”


数字4:通过中间件而不是在中间件内集中处理错误

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错误处理”排名第一)

“…您可能会在堆栈的多个级别处理相同的错误。当较低级别无法执行任何有用的操作时,就会发生这种情况,除非将错误传播到其调用方,然后将错误传播给其调用方等等。”。通常,只有顶级调用方知道适当的响应是什么,是重试操作、向用户报告错误还是其他什么。但这并不意味着您应该尝试向单个顶级回调报告所有错误,因为回调本身无法知道错误发生在什么上下文中”


数字5:使用Swagger记录API错误

TL;DR:让您的API调用方知道可能会返回哪些错误,这样他们就可以谨慎地处理这些错误而不会崩溃。这通常使用诸如Swagger之类的REST API文档框架来完成

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

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

…我们已经讨论了如何处理错误,但当您编写新函数时,如何将错误传递给调用您函数的代码…如果你不知道会发生什么错误,或者不知道错误的含义,那么你的程序就不可能是正确的,除非是偶然的。因此,如果您正在编写一个新函数,您必须告诉调用者可能发生的错误以及它们的含义


数字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。让应用程序崩溃并重新启动它。处理所有可能的错误,永远不要崩溃。3.两者之间的平衡方法


数字7:使用成熟的记录器提高错误可见性

TL;DR:一套成熟的测井工具,如Winston、Bunyan或Log4J,将加快错误发现和理解。所以忘掉console.log吧。

否则:在没有查询工具或像样的日志查看器的情况下,浏览console.logs或手动浏览杂乱的文本文件可能会让你忙到很晚

代码示例-Winston logger在运行

//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.允许多个可配置的目标流。例如,您可能正在将跟踪日志写入一个文件,但当遇到错误时,请写入同一文件,然后写入错误文件并同时发送电子邮件…


数字8:使用APM产品发现错误和停机

TL;DR:监控和性能产品(也称APM)主动评估您的代码库或API,以便它们能够神奇地自动突出错误、崩溃和您丢失的慢部件

否则:您可能会花费大量精力来衡量API性能和停机时间,可能您永远不会意识到在真实场景下哪些是您最慢的代码部分,以及这些部分如何影响用户体验

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

“…APM产品包括三个主要部分:1。网站或API监控–通过HTTP请求不断监控正常运行时间和性能的外部服务。可以在几分钟内设置。以下是几个被选中的竞争者:Pingdom、Uptime Robot和New Relic2.代码检测–需要在应用程序中嵌入代理以帮助实现慢速代码检测、异常统计、性能监控等功能的产品系列。以下是几个选定的竞争者:NewRelic、AppDynamics3.运营智能仪表板–这些产品线专注于通过指标和精心策划的内容帮助运营团队轻松掌握应用程序性能。这通常涉及聚合多个信息源(应用程序日志、数据库日志、服务器日志等)和前期仪表板设计工作。以下是一些选定的竞争者:Datadog、Splunk“


以上是一个简短的版本-请参阅此处的更多最佳实践和示例

其他回答

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

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

如果你想在Ubuntu中使用服务(Upstart):在Ubuntu 11.04中使用节点作为服务,包括Upstart、monit和forever.js

前段时间读了这篇文章后,我想知道在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”处理程序抛出异常,在我看来,这是一个交易破坏者。