Node.js最佳实践异常处理
几天前我刚开始尝试使用node.js。 我意识到,只要我的程序中有未处理的异常,Node就会被终止。 这与我已经接触到的正常服务器容器不同,当只有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
是在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
以下是关于此主题的许多不同来源的汇总和策展,包括来自选定博客文章的代码示例和引用。 这里可以找到完整的最佳实践列表
Node.JS错误处理的最佳实践
Number1:使用承诺进行异步错误处理
TL; DR:处理回调风格中的异步错误可能是最糟糕的地狱之路(又名厄运金字塔)。 你可以给你的代码最好的礼物是使用一个有信誉的承诺库,它提供了非常紧凑和熟悉的代码语法,如try-catch
否则: Node.JS回调风格,函数(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)
“......事实上,回调的做法更加险恶:他们剥夺了我们的堆栈,这在编程语言中我们通常认为是理所当然的。无需堆栈编写代码就像在没有刹车踏板的情况下驾驶汽车:您没有意识到你需要它多么糟糕,直到你达到它并且它不在那里。 承诺的全部重点是让我们回到我们在异步时丢失的语言基础:返回,抛出和堆栈。必须知道如何正确使用承诺以利用它们。 “
Number2:只使用内置的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?");
博客引用:“字符串不是错误” (来自博客devthought,关键字“Node.JS错误对象”排名6)
“...传递字符串而不是错误会导致模块之间的互操作性降低,它违反了可能正在执行instanceof错误检查的API或者想要更多地了解错误的API 。除了将消息传递给构造函数之外,现代JavaScript引擎中有趣的属性......“
Number3:区分操作与程序员错误
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未捕获的异常”)
“ ......由于JavaScript的投掷方式的本质,几乎没有任何方法可以安全地”拾取你离开的地方“,而不会泄漏引用或创建其他某种未定义的脆弱状态。一个抛出的错误是关闭进程当然,在一个普通的Web服务器中,你可能会打开很多连接,并且由于其他人触发错误而突然关闭这些连接是不合理的,更好的方法是向引发错误的请求发送错误响应,同时让其他人在正常时间内完成,并停止监听该工作人员中的新请求“
Number4:集中处理错误,但不在中间件内
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错误处理”排名第一)
“......你可能最终会在堆栈的几个层次上处理相同的错误,这种情况发生在较低级别除了将错误传播给其调用者(将错误传播给调用者)等等之外,只有顶级调用者知道适当的响应是什么,无论是重试操作,向用户报告错误还是其他内容,但这并不意味着您应该尝试将所有错误报告给单个顶级回调,因为回调本身无法知道错误发生在什么情况下“
Number5:使用Swagger记录API错误
TL; DR:让您的API调用者知道哪些错误可能会返回,以便他们能够在不崩溃的情况下谨慎处理这些错误。 这通常使用Swagger等REST API文档框架完成
否则: API客户端可能会决定崩溃并重新启动,因为他收到了他无法理解的错误。 注意:您的API的调用者可能是您(在微服务环境中非常典型)
博客引用: “你必须告诉你的调用者会发生什么错误”(来自Joyent博客,关键字“Node.JS logging”排名第一)
...我们已经讨论了如何处理错误,但是当您编写一个新函数时,您如何向调用函数的代码传递错误? ...如果你不知道会发生什么错误或不知道他们的意思,那么你的程序就不会是正确的,除非意外。 所以,如果你正在编写一个新的功能,你必须告诉你的呼叫者什么样的错误会发生,他们是什么
Number6:当一个陌生人来到城里时,优雅地关闭这个过程
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.两者平衡
Number7:使用成熟的记录器来增加错误的可见性
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' });
博客引用: “让我们识别一些需求(对于记录器):”(来自博客的强博)
...让我们识别一些要求(对于记录器):1.时间戳记每条记录行。 这个很自我解释 - 你应该能够知道每个日志条目何时发生。 2.记录格式应该容易被人类和机器消化。 3.允许多个可配置的目标流。 例如,您可能会将跟踪日志写入一个文件,但遇到错误时,请写入相同的文件,然后写入错误文件并同时发送电子邮件...
Number8:使用APM产品发现错误和停机时间
TL; DR:监控和性能产品(又名APM)主动评估您的代码库或API,以便它们可以自动奇迹般突出显示错误,崩溃并放慢您缺少的部分
否则:您可能会花很多精力测量API性能和停机时间,可能您永远不会意识到哪些是您在现实世界场景下最慢的代码部分,以及这些影响UX的方式
博客引用: “APM产品细分”(来自Yoni Goldberg博客)
“...... APM产品构成了3个主要部分: 1.网站或API监控 -通过HTTP请求持续监控正常运行时间和性能的外部服务可在几分钟内完成设置以下几种选择竞争者:Pingdom,Uptime Robot和New Relic 2 。代码装备-产品系列需要嵌入在应用程序中的代理受益特征慢的代码检测,异常统计,性能监控和许多以下是几个选定的竞争者:New Relic的,应用程序动态3.业务智能仪表板-这些线。的产品侧重于帮助ops团队提供指标和策划的内容,这些内容有助于轻松保持应用程序的性能,这通常涉及将多个信息源(应用程序日志,数据库日志,服务器日志等)和前期仪表板设计以下是少数选择的竞争者:Datadog,Splunk“
以上是缩短版本 - 请参阅此处的更多最佳做法和示例
您可以捕获未捕获的异常,但使用有限。 请参阅http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions:4c933d54-1428-443c-928d-4e1ecbdd56cb
当它崩溃时,可以使用monit
, forever
或upstart
来重新启动节点进程。 正常关机最好是你可以希望的(例如,将所有内存数据保存在未捕获的异常处理程序中)。