我刚开始尝试使用node.js。我已经意识到只要我的程序中有未处理的异常,Node就会终止。这与我所暴露的普通服务器容器不同,只有当未处理的异常发生且容器仍然能够接收请求时,工作线程才会死亡。这引出了一些问题:
process.on('uncaughtException')
是防范它的唯一有效方法吗? process.on('uncaughtException')
是否会捕获未处理的异常?我很感激任何指针/文章,它会向我展示在node.js中处理未捕获的异常的常见最佳实践
答案 0 :(得分:700)
更新:Joyent现在有their own guide。以下信息更多的是摘要:
理想情况下,我们希望尽可能避免未被捕获的错误,因此,我们可以使用以下方法之一安全地“抛出”错误,而不是根据我们的代码架构安全地“抛出”错误:
对于同步代码,如果发生错误,请返回错误:
// 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)
}
})
对于eventful代码,错误可能发生在任何地方,而不是抛出错误,触发error
event instead:
// 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 domain
中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
侦听器使我们的应用程序不崩溃(但这样做可以将应用程序放在unknown state)中:
// 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
答案 1 :(得分:75)
以下是关于此主题的许多不同来源的摘要和管理,包括代码示例和所选博客帖子的引用。完整的最佳做法列表can be found here
TL; DR:处理回调样式中的异步错误可能是地狱的最快方式(a.k.a是厄运的金字塔)。您可以为代码提供的最佳礼物是使用信誉良好的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" Node Promises")
" ...事实上,回调做了一些更加险恶的事情:它们剥夺了我们的堆栈,这是我们在编程语言中通常认为理所当然的事情。没有堆叠编写代码就像驾驶没有刹车踏板的汽车一样:你没有意识到你需要它有多么糟糕,直到你达到它并且它不在那里。 承诺的重点是让我们回到我们去异步时丢失的语言基础:return,throw和stack。但你必须知道如何正确使用promises才能利用它们。"
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?");
博客引用:"字符串不是错误" (来自博客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);
});
博客报价:"否则您冒险了州" (来自博客可调试,关键字排名第3" Node.JS未捕获异常")
" ...根据throw如何在JavaScript中工作的本质,几乎从来没有任何方法可以安全地“拾取你离开的地方”,不会泄漏引用或创建其他类型的未定义脆弱的状态。响应抛出错误的最安全方法是关闭进程。当然,在普通的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错误处理”排名第1)
" ...您最终可能会在堆栈的多个级别处理相同的错误。当较低级别无法执行任何有用的操作时会发生这种情况,除非将错误传播给调用方,调用方将错误传播给调用方,依此类推。通常,只有顶级调用者知道适当的响应是什么,是重试操作,向用户报告错误还是其他内容。但这并不意味着您应该尝试将所有错误报告给单个顶级回调,因为该回调本身无法知道错误发生在什么情况下"
TL; DR:让您的API调用者知道可能会返回哪些错误,以便他们可以周到地处理这些错误而不会崩溃。这通常通过REST API文档框架(如Swagger
)完成否则: API客户端可能会因为收到他无法理解的错误而决定崩溃并重新启动。注意:API的调用者可能是您(在微服务环境中非常典型)
博客引用"您必须告诉您的来电者可能发生的错误" (来自博客Joyent,关键字“Node.JS logging”排名第1)
...我们已经讨论过如何处理错误,但是当您编写新函数时,如何将错误传递给调用函数的代码? ...如果您不知道可能发生什么错误或者不知道它们的含义,那么除非意外,否则您的程序将无法正确执行。因此,如果您正在编写新功能,则必须告诉您的呼叫者可能发生的错误以及它们的用途
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;
}
博客引用:"有三种关于错误处理的思路和#34; (来自博客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:监控和性能产品(a.k.a APM)主动评估您的代码库或API,以便他们可以自动神奇地突出显示您丢失的错误,崩溃和慢速部件
否则:您可能会花费大量精力来衡量API性能和停机时间,可能您永远不会意识到哪些是您在现实世界场景中最慢的代码部分以及它们如何影响用户体验
博客报价:" APM产品细分" (来自博客Yoni Goldberg)
" ... APM产品构成3个主要部分: 1。网站或API监控 - 通过HTTP请求持续监控正常运行时间和性能的外部服务。可以在几分钟内完成设置。以下是几个选定的竞争者:Pingdom,Uptime Robot和New Relic 的 2。代码检测 - 产品系列,需要在应用程序中嵌入代理,以实现慢速代码检测,异常统计,性能监控等功能。以下是几个选定的竞争者:New Relic,App Dynamics 第3。运营智能仪表板 - 这些产品系列专注于为运营团队提供指标和策划内容,以帮助轻松掌握应用程序性能。这通常涉及聚合多个信息源(应用程序日志,数据库日志,服务器日志等)和前期仪表板设计工作。以下几个选定的竞争者:Datadog,Splunk"
答案 2 :(得分:29)
您可以捕获未捕获的异常,但它的用途有限。见http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions:4c933d54-1428-443c-928d-4e1ecbdd56cb
monit
,forever
或upstart
可用于在崩溃时重新启动节点进程。您可以期望正常关闭(例如,在未捕获的异常处理程序中保存所有内存数据)。
答案 3 :(得分:14)
nodejs domains是处理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):
上面,我使用了一个暗示fibers semantics的未来,它允许你在线上等待期货。这实际上允许您使用传统的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延迟了 - “失败”处理程序永远不会抛出一个“当时”处理程序,在我看来这是一个交易破坏者。
答案 4 :(得分:12)
我最近在http://snmaynard.com/2012/12/21/node-error-handling/写了这篇文章。版本0.8中的节点的新功能是域,允许您将所有形式的错误处理组合到一个更简单的管理表单中。你可以在我的帖子中阅读它们。
您还可以使用Bugsnag之类的内容来跟踪未捕获的异常情况,并通过电子邮件,聊天室通知或为未捕获的异常创建故障单(我是Bugsnag的联合创始人)。
答案 5 :(得分:4)
使用try-catch可能适合的一个实例是使用forEach循环时。它是同步的,但同时你不能在内部范围中使用return语句。相反,可以使用try和catch方法在适当的范围内返回Error对象。考虑一下:
function processArray() {
try {
[1, 2, 3].forEach(function() { throw new Error('exception'); });
} catch (e) {
return e;
}
}
这是上述@balupton描述的方法的组合。
答案 6 :(得分:3)
我想补充一点,Step.js library通过将异常传递给下一步功能来帮助您处理异常。因此,您可以使用一个函数来检查上述任何步骤中的任何错误。这种方法可以大大简化您的错误处理。
以下是github页面的引用:
抛出的任何异常都被捕获并作为第一个参数传递给 下一个功能。只要你不嵌入回调函数嵌入 你的主要功能,以防止任何未被捕获 例外。这对于长期运行的node.JS服务器非常重要 因为一个未捕获的异常会导致整个服务器崩溃。
此外,您可以使用Step来控制脚本的执行,以便将清理部分作为最后一步。例如,如果你想在Node中编写一个构建脚本并报告编写所花费的时间,那么最后一步就可以做到(而不是试图挖出最后一个回调)。
答案 7 :(得分:2)
前一段时间阅读这篇文章后,我想知道在api /功能级别上使用域进行异常处理是否安全。我想用它们来简化我写的每个异步函数中的异常处理代码。我担心的是,为每个函数使用一个新域会带来很大的开销。我的作业似乎表明开销很小,而且在某些情况下,域名的性能实际上比使用try catch更好。
http://www.lighthouselogic.com/#/using-a-new-domain-for-each-async-function-in-node/
答案 8 :(得分:1)
如果您想在Ubuntu(Upstart)中使用服务:Node as a service in Ubuntu 11.04 with upstart, monit and forever.js
答案 9 :(得分:1)
此处已经非常好地讨论了捕获错误,但值得记住将错误记录到某处,以便您可以查看它们并修复内容。
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这样的工具或永远使用,你的应用程序应该能够崩溃,注销发生的事情并重启而不会出现任何重大问题。
答案 10 :(得分:0)
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,
}
},