[英]Node.js Best Practice Exception Handling
几天前我刚开始尝试 node.js。 我意识到只要我的程序中有未处理的异常,节点就会终止。 这与我接触过的普通服务器容器不同,其中只有 Worker Thread 在发生未处理的异常时死亡,并且容器仍然能够接收请求。 这提出了几个问题:
process.on('uncaughtException')
是唯一有效的防范方法吗?process.on('uncaughtException')
也会在异步进程执行期间捕获未处理的异常?我将不胜感激任何能向我展示在 node.js 中处理未捕获异常的常见最佳实践的指针/文章
更新: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) }
对于基于回调(即异步)的代码,回调的第一个参数是err
,如果发生err
是错误,如果没有发生错误则err
是null
。 任何其他参数都跟在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)
但有时,可能仍然有代码在某处抛出错误,如果我们不安全地捕获它,可能会导致未捕获的异常和我们的应用程序潜在的崩溃。 根据我们的代码架构,我们可以使用以下方法之一来捕获它:
当我们知道错误发生在哪里时,我们可以将该部分包装在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) }
但是,请注意不要在异步代码中使用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 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) 异步代码中未捕获的异常 (2) try catch 捕获您不希望的执行。 在允许正确线程而不是 JavaScript 的异步事件机风格的语言中,这不是一个问题。
最后,如果未捕获的错误发生在未包含在域或 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
以下是针对此主题的许多不同来源的总结和整理,包括代码示例和来自选定博客文章的引用。 可以在此处找到最佳实践的完整列表
TL;DR:以回调方式处理异步错误可能是最快的方法(也就是厄运金字塔)。 你能给你的代码最好的礼物是使用一个信誉良好的承诺库,它提供了很多紧凑和熟悉的代码语法,比如 try-catch
否则:由于错误处理与随意代码、过度嵌套和笨拙的编码模式的混合,Node.JS 回调风格、function(err, response) 是不可维护代码的一种有前途的方式
代码示例 - 好
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)
“......事实上,回调做了一些更险恶的事情:它们剥夺了我们在编程语言中通常认为理所当然的堆栈。编写没有堆栈的代码很像在没有刹车踏板的情况下驾驶汽车:你不要意识到你有多需要它,直到你伸手去拿它,但它不在那里。承诺的全部意义是把我们在异步时失去的语言基础还给我们:返回、抛出和堆栈。但是你必须知道如何正确使用 Promise 才能利用它们。 ”
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?");
博客引用:“A string is not an error” (来自博客 devthought,关键字“Node.JS 错误对象”排名第 6)
“……传递字符串而不是错误会导致模块之间的互操作性降低。它破坏了与可能正在执行错误检查实例或想要了解更多错误信息的 API 的契约。正如我们将看到的,错误对象具有非常除了保存传递给构造函数的消息之外,现代 JavaScript 引擎中还有一些有趣的属性……”
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);
});
博客引用:“否则你会冒险”(来自博客 debugable,关键字“Node.JS 未捕获异常”排名第 3)
“ ...根据 JavaScript 中 throw 工作方式的本质,几乎没有任何方法可以安全地“从上次中断的地方继续”,而不会泄漏引用或创建其他某种未定义的脆弱状态。最安全的响应方式抛出的错误是关闭进程。当然,在正常的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 错误处理”排名第一)
“……您可能最终会在堆栈的多个级别处理相同的错误。当较低级别无法做任何有用的事情时就会发生这种情况,除了将错误传播给他们的调用者,然后将错误传播给它的调用者,依此类推。通常,只有顶级调用者知道适当的响应是什么,无论是重试操作、向用户报告错误还是其他什么。但这并不意味着您应该尝试将所有错误报告给单个顶级回调,因为回调本身无法知道错误发生在什么上下文中”
TL;DR:让您的 API 调用者知道哪些错误可能会返回,以便他们可以周到地处理这些错误而不会崩溃。 这通常是通过像 Swagger 这样的 REST API 文档框架来完成的
否则: API 客户端可能会因为收到一个他无法理解的错误而决定崩溃并重新启动。 注意:您的 API 的调用者可能是您(在微服务环境中非常典型)
博客引用: “你必须告诉你的调用者会发生什么错误”(来自博客 Joyent,关键词“Node.JS logging”排名第一)
…我们已经讨论了如何处理错误,但是当您编写一个新函数时,您如何将错误传递给调用您的函数的代码? …如果您不知道会发生什么错误或不知道它们的含义,那么您的程序不可能是正确的,除非是偶然的。 所以如果你正在编写一个新函数,你必须告诉你的调用者可能会发生什么错误以及它们的含义
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. 两者兼顾
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. 允许多个可配置的目标流。 例如,您可能正在将跟踪日志写入一个文件,但是当遇到错误时,写入同一个文件,然后写入错误文件并同时发送一封电子邮件......
TL;DR:监控和性能产品(又名 APM)主动评估您的代码库或 API,以便它们可以自动神奇地突出显示您遗漏的错误、崩溃和缓慢的部分
否则:您可能会花费大量精力来衡量 API 性能和停机时间,可能您永远不会意识到在现实世界场景中哪些是最慢的代码部分以及它们如何影响 UX
博客引用: “APM 产品细分”(来自博客 Yoni Goldberg)
“……APM 产品由 3 个主要部分构成: 1. 网站或 API 监控——通过 HTTP 请求持续监控正常运行时间和性能的外部服务。可以在几分钟内完成设置。以下是几个选定的竞争者:Pingdom、Uptime Robot 和 New Relic 2 . 代码检测 –需要在应用程序中嵌入代理以实现慢速代码检测、异常统计、性能监控等功能的产品系列。以下是少数选定的竞争者:New Relic、App Dynamics 3. 运营智能仪表板 –这些产品线的产品专注于通过指标和精选内容为运营团队提供便利,这些内容有助于轻松掌握应用程序性能。这通常涉及聚合多个信息源(应用程序日志、数据库日志、服务器日志等)和前期仪表板设计工作。以下是少数选定的竞争者:Datadog、Splunk”
以上是一个缩短的版本 - 在这里查看更多最佳实践和示例
您可以捕获未捕获的异常,但它的用途有限。 见http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions:4c933d54-1428-443c-928d-4e1ecbdd56cb
monit
、 forever
或upstart
可用于在节点崩溃时重新启动节点进程。 正常关闭是您所希望的最佳方式(例如,将所有内存中的数据保存在未捕获的异常处理程序中)。
nodejs 域是处理 nodejs 中错误的最新方法。 域可以捕获错误/其他事件以及传统上抛出的对象。 域还提供了处理回调的功能,错误是通过拦截方法作为第一个参数传递的。
与正常的 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”,因为引发了中断该流程的异常。
看看蓝鸟的承诺:
请注意,除了这些可以正确处理抛出的异常的库之外,我还没有找到许多其他库。 例如,jQuery 的延迟不会 - “失败”处理程序永远不会抛出异常并抛出“然后”处理程序,在我看来,这是一个交易破坏者。
我最近在http://snmaynard.com/2012/12/21/node-error-handling/ 上写了这篇文章。 0.8 版本中 node 的一个新特性是域,它允许您将所有错误处理形式组合成一个更容易管理的形式。 您可以在我的帖子中阅读有关它们的信息。
您还可以使用Bugsnag 之类的工具来跟踪未捕获的异常,并通过电子邮件、聊天室收到通知,或者为未捕获的异常创建票证(我是 Bugsnag 的联合创始人)。
使用 try-catch 可能合适的一种情况是使用 forEach 循环。 它是同步的,但同时你不能只在内部范围内使用 return 语句。 相反,可以使用 try 和 catch 方法在适当的范围内返回一个 Error 对象。 考虑:
function processArray() {
try {
[1, 2, 3].forEach(function() { throw new Error('exception'); });
} catch (e) {
return e;
}
}
它是上面@balupton 描述的方法的组合。
我只想补充一点, Step.js 库通过始终将异常传递给下一步函数来帮助您处理异常。 因此,您可以在最后一步使用一个函数来检查前面任何步骤中的任何错误。 这种方法可以大大简化您的错误处理。
以下是来自 github 页面的引用:
任何抛出的异常都会被捕获并作为第一个参数传递给下一个函数。 只要您不嵌套回调函数内联您的主要函数,这可以防止出现任何未捕获的异常。 这对于长时间运行的 node.JS 服务器非常重要,因为一个未捕获的异常可能会导致整个服务器宕机。
此外,您可以使用 Step 来控制脚本的执行,将清理部分作为最后一步。 例如,如果你想在 Node 中写一个构建脚本并报告写了多长时间,最后一步可以这样做(而不是试图挖掘出最后一个回调)。
捕获错误在这里已经讨论得很好,但值得记住的是将错误记录在某个地方,以便您可以查看它们并修复它们。
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 之类的工具交叉使用或永远使用,您的应用程序应该能够崩溃,注销发生的事情并重新启动而不会出现任何重大问题。
前段时间阅读这篇文章后,我想知道在 api / 函数级别使用域进行异常处理是否安全。 我想使用它们来简化我编写的每个异步函数中的异常处理代码。 我担心为每个功能使用一个新域会引入大量开销。 我的作业似乎表明开销很小,并且在某些情况下域的性能实际上比 try catch 更好。
http://www.lighthouselogic.com/#/using-a-new-domain-for-each-async-function-in-node/
如果你想在 Ubuntu(Upstart) 中使用服务: Node as a service in Ubuntu 11.04 with upstart, monit and ever.js
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,
}
},
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.