Node.js最佳实践异常处理

我几天前才开始尝试node.js。我意识到每当我的程序中出现未处理的异常时,Node都会终止。这与我接触过的普通服务器容器不同,在普通服务器容器中,当发生未处理的异常时,只有工作线程死亡,容器仍然能够接收请求。这引发了几个问题:

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

我将感谢任何指针/文章,它将向我展示处理node.js中未捕获异常的常见最佳实践

575085 次浏览

您可以捕获未捕获的异常,但它的用途有限。请参阅http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions: 4c933d54-1428-443c-928d-4e1ecbdd56cb

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

更新:Joyent现在有自己的指南。以下信息更像是摘要:

安全地“抛出”错误

理想情况下,我们希望尽可能避免未捕获的错误,因此,我们可以使用以下方法之一安全地“抛出”错误,而不是从字面上抛出错误,具体取决于我们的代码架构:

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

    // Define divider as a syncrhonous functionvar divideSync = function(x,y) {// if error condition?if ( y === 0 ) {// "throw" the error safely by returning itreturn new Error("Can't divide by zero")}else {// no error occured, continue onreturn x/y}}
    // Divide 4/2var result = divideSync(4,2)// did an error occur?if ( result instanceof Error ) {// handle the error safelyconsole.log('4/2=err', result)}else {// no error occured, continue onconsole.log('4/2='+result)}
    // Divide 4/0result = divideSync(4,0)// did an error occur?if ( result instanceof Error ) {// handle the error safelyconsole.log('4/0=err', result)}else {// no error occured, continue onconsole.log('4/0='+result)}
  • For callback-based (ie. asynchronous) code, the first argument of the callback is err, if an error happens err is the error, if an error doesn't happen then err is null. Any other arguments follow the err argument:

    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 errornext(new Error("Can't divide by zero"))}else {// no error occured, continue onnext(null, x/y)}}
    divide(4,2,function(err,result){// did an error occur?if ( err ) {// handle the error safelyconsole.log('4/2=err', err)}else {// no error occured, continue onconsole.log('4/2='+result)}})
    divide(4,0,function(err,result){// did an error occur?if ( err ) {// handle the error safelyconsole.log('4/0=err', err)}else {// no error occured, continue onconsole.log('4/0='+result)}})
  • For eventful code, where the error may happen anywhere, instead of throwing the error, fire the error event instead:

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

Safely "catching" errors

Sometimes though, there may still be code that throws an error somewhere which can lead to an uncaught exception and a potential crash of our application if we don't catch it safely. Depending on our code architecture we can use one of the following methods to catch it:

  • When we know where the error is occurring, we can wrap that section in a node.js domain

    var d = require('domain').create()d.on('error', function(err){// handle the error safelyconsole.log(err)})
    // catch the uncaught errors in this asynchronous or synchronous code blockd.run(function(){// the asynchronous or synchronous code that we want to catch thrown errors onvar err = new Error('example')throw err})
  • If we know where the error is occurring is synchronous code, and for whatever reason can't use domains (perhaps old version of node), we can use the try catch statement:

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

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

    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与异步代码结合使用,那么在运行Node 7.4或更高版本时,您可以本地使用async/await来编写异步函数。

    使用try...catch要小心的另一件事是将完成回调包装在try语句中的风险,如下所示:

    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 errornext(new Error("Can't divide by zero"))}else {// no error occured, continue onnext(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)异步代码中未捕获的异常(2)try catch捕获你不希望它执行的执行。在允许正确线程而不是JavaScript的异步事件机器风格的语言中,这不是一个问题。

  • 最后,如果一个未捕获的错误发生在一个没有被域或try cat语句包装的地方,我们可以使用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 boundprocess.on('uncaughtException', function(err) {// handle the error safelyconsole.log(err)})
    // the asynchronous or synchronous code that emits the otherwise uncaught errorvar err = new Error('example')throw err

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

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

抛出的任何异常都会被捕获并作为第一个参数传递给下一个函数。只要您不内嵌回调函数你的主要功能是防止任何未捕获的东西这对于长时间运行的node.JS服务器非常重要因为一个未捕获的异常会导致整个服务器崩溃。

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

如果您想在Ubuntu(Upstart)中使用服务:Ubuntu 11.04中的节点即服务,带有upstart、monit和forever.js

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

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

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

我最近在http://snmaynard.com/2012/12/21/node-error-handling/上写过这个。0.8版中节点的一个新功能是域,允许您将所有形式的错误处理组合成一个更容易管理的表单。您可以在我的帖子中阅读它们。

您还可以使用漏洞之类的东西来跟踪未捕获的异常,并通过电子邮件、聊天室收到通知,或者为未捕获的异常创建票证(我是Bugsnag的联合创始人)。

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

与正常的try/catch-style错误处理一样,通常最好在错误发生时抛出错误,并阻止希望隔离错误以影响其余代码的区域。“阻止”这些区域的方法是使用函数调用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 stuffvar future = somethingAsynchronous();// more stuff
future.wait(); // here we care about the errorif(err != null) {// handle data reversion// probably log too}
})});

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

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

更新(2013-09):

在上面,我使用了一个暗示纤维语义学的未来,它允许你在线等待期货。这实际上允许你为一切使用传统的尝试捕获块——我发现这是最好的方法。然而,你不能总是这样做(即在浏览器中)…

还有一些期货不需要纤维语义学(然后使用普通的浏览器化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,即使这些片段是异步的。它会打印:

12handler

请注意,它不会打印'3',因为抛出了中断该流的异常。

看看蓝鸟承诺:

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

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

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

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或永远这样的工具交叉,您的应用程序应该能够崩溃,注销发生的事情并重新启动而没有任何重大问题。

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


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


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

太长别读:以回调风格处理异步错误可能是最快的地狱方式(又名厄运金字塔)。你能给你的代码的最好礼物是使用一个信誉良好的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 errorgetMoreData(a, function(err, result){if(err != null)//do something like calling the given callback function and pass the errorgetMoreData(b, function(c){getMoreData(d, function(e){...});});});});});

博客引用:“我们对承诺有问题”(来自博客pouchdb,关键词“Node Promises”排名第11)

“……事实上,回调做了更险恶的事情:它们剥夺了我们的堆栈,这是我们在编程语言中通常认为理所当然的事情。编写没有堆栈的代码就像驾驶没有刹车踏板的汽车:你没有意识到你有多需要它,直到你伸手去拿它,它不在那里。Promise的全部意义在于将我们在异步时丢失的语言基础还给我们:返回、抛出和堆栈。但是您必须知道如何正确使用Promise才能利用它们。


Number2:仅使用内置的Error对象

太长别读:将错误作为字符串或自定义类型抛出的代码很常见-这会使错误处理逻辑和模块之间的互操作性复杂化。无论你是拒绝Promise、抛出异常还是发出错误-使用Node.JS内置的Error对象都会增加一致性并防止错误信息的丢失

否则:当执行某个模块时,不确定返回的是哪种类型的错误-使得推断即将到来的异常并处理它变得更加困难。甚至值得,使用自定义类型来描述错误可能会导致丢失关键错误信息,如堆栈跟踪!

代码示例-做得对

    //throwing an Error from typical function, whether sync or asyncif(!productToAdd)throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitterconst myEmitter = new MyEmitter();myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promisereturn 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 propertiesif(!productToAdd)throw ("How can I add new product when no value provided?");

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

"…传递字符串而不是错误会降低模块之间的互操作性。它破坏了与可能正在执行instance of Error检查或希望了解更多错误信息的API的合同。正如我们将看到的,错误对象在现代JavaScript引擎中具有非常有趣的属性,除了保存传递给构造函数的消息…”


数字3:区分操作错误和程序员错误

太长别读:操作错误(例如API收到无效输入)是指错误影响已经得到充分理解并且可以深思熟虑地处理的已知情况。另一方面,程序员错误(例如尝试读取未定义的变量)是指未知的代码故障,这决定了优雅地重新启动应用程序

否则:当错误出现时,你可能总是重新启动应用程序,但是为什么会因为一个轻微的和预测到的错误(操作错误)而让~5000个在线用户失望呢?相反的情况也不理想——在未知问题(程序员错误)发生时保持应用程序正常运行可能会导致不可预测的行为。区分两者允许巧妙地采取行动,并根据给定的上下文应用平衡的方法

代码示例-做得对

    //throwing an Error from typical function, whether sync or asyncif(!productToAdd)throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitterconst myEmitter = new MyEmitter();myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promisereturn 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 operationalvar 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 middlewareprocess.on('uncaughtException', function(error) {if(!error.isOperational)process.exit(1);});

博客报价:“否则,您将面临国家风险”(来自可调试的博客,关键词“Node.JS未捕获异常”排名第3)

"…就JavaScript中抛出的工作方式而言,几乎没有任何方法可以安全地“从你离开的地方捡起”,而不会泄露引用,或创建其他类型的未定义的脆弱状态。响应抛出的错误的最安全方法是关闭进程。当然,在普通的Web服务器中,您可能打开了许多连接,因为其他人触发了错误而突然关闭这些连接是不合理的。更好的方法是向触发错误的请求发送错误响应,同时让其他人在正常时间内完成,并停止监听该工作进程中的新请求"


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

太长别读:错误处理逻辑,例如邮件到管理员和日志记录,应该封装在一个专用的集中式对象中,当出现错误时,所有端点(例如Express中间件、cron作业、单元测试)都会调用该对象。

否则:不处理单个位置内的错误会导致代码重复,并可能导致处理不当的错误

代码示例-典型的错误流

//DAL layer, we don't handle errors hereDB.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 middlewaretry {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 handlerapp.use(function (err, req, res, next) {errorHandler.handleError(err).then((isOperationalError) => {if (!isOperationalError)next(err);});});

博客引用:"有时较低的级别除了将错误传播给他们的调用者之外不能做任何有用的事情"(来自Joyent博客,关键词“Node.JS错误处理”排名第一)

"…你最终可能会在堆栈的几个级别处理相同的错误。当较低级别除了将错误传播给调用者之外无法做任何有用的事情时,就会发生这种情况,调用者会将错误传播给调用者,等等。通常,只有顶级调用者知道适当的响应是什么,是重试操作、向用户报告错误还是其他什么。但这并不意味着你应该尝试将所有错误报告给一个顶级回调,因为回调本身不知道错误是在什么上下文中发生的"


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

太长别读:让您的API调用者知道哪些错误可能会返回,这样他们就可以在不崩溃的情况下深思熟虑地处理这些错误。这通常是通过像Swagger这样的REST API留档框架完成的

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

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

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


数字6:当陌生人进城时,优雅地关闭进程

太长别读:当发生未知错误(开发人员错误,请参阅最佳实践#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 #3process.on('uncaughtException', function(error) {errorManagement.handler.handleError(error);if(!errorManagement.handler.isTrustedError(error))process.exit(1)});

//centralized error handler encapsulates error-handling related logicfunction errorHandler(){this.handleError = function (error) {return logger.logError(err).then(sendMailToAdminIfCritical).then(saveInOpsQueueIfCritical).then(determineIfOperationalError);}
this.isTrustedError = function(error){return error.isOperational;}

博客引用:"关于错误处理有三种思想流派"(来自博客js食谱)

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


数字7:使用成熟的记录器来增加错误可见性

太长别读:一套成熟的日志工具,如Winston、Bunyan或Log4J,将加快错误发现和理解。所以忘了console.log.

否则:在没有查询工具或体面的日志查看器的情况下浏览console.logs或手动浏览凌乱的文本文件可能会让您在工作中忙碌到很晚

代码示例-Winston logger in action

//your centralized logger objectvar logger = new winston.Logger({level: 'info',transports: [new (winston.transports.Console)(),new (winston.transports.File)({ filename: 'somefile.log' })]});
//custom code somewhere using the loggerlogger.log('info', 'Test Log Message with some parameter %s', 'some parameter', { anything: 'This is metadata' });

博客引用:"让我们确定一些要求(对于记录器):"(来自Blog)

…让我们确定一些要求(对于记录器):1.每个日志行的时间戳。这是一个非常不言自明的-你应该能够分辨出每个日志条目发生的时间。2.日志格式应该是人类和机器都容易消化的。3.允许多个可配置的目标流。例如,您可能将跟踪日志写入一个文件,但当遇到错误时,写入同一个文件,然后写入错误文件并同时发送电子邮件…


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

太长别读:监控和性能产品(也称为APM)主动评估您的代码库或API,以便它们可以神奇地自动突出显示您缺少的错误、崩溃和慢速部分

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

博客引用:"APM产品细分市场"(来自Yoni Goldberg博客)

"… APM产品由3个主要部分组成:1.网站或API监控-外部服务,通过HTTP请求不断监控正常运行时间和性能。可以在几分钟内设置。以下是一些选定的竞争者:P的、Uptime Robot和New Relic2.代码检测-产品系列需要在应用程序中嵌入代理才能受益,具有慢代码检测、异常统计、性能监控等功能。以下是几个选定的竞争者:New Relic、App Dynamics3.运营智能仪表板-这些产品线专注于为运营团队提供指标和精选内容,帮助他们轻松掌握应用程序性能。这通常涉及聚合多个信息源(应用程序日志、数据库日志、服务器日志等)和前期仪表板设计工作。以下是几个选定的竞争者:Datadog、Splunk"


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

  getCountryRegionData: (countryName, stateName) => {let countryData, stateData
try {countryData = countries.find(country => country.countryName === countryName)} catch (error) {console.log(error.message)return error.message}
try {stateData = countryData.regions.find(state => state.name === stateName)} catch (error) {console.log(error.message)return error.message}
return {countryName: countryData.countryName,countryCode: countryData.countryShortCode,stateName: stateData.name,stateCode: stateData.shortCode,}},