这段代码理论上能防止 NodeJS 应用程序崩溃吗?
这段代码理论上能防止 NodeJS 应用程序崩溃吗?
几天前我开始尝试使用node.js。我意识到,只要我的程序发生未处理的异常,Node就会终止。这与我接触过的普通服务器容器不同,那里仅在出现未处理异常时,工作线程才会停止,然而容器仍然能够接收请求。这引起了一些问题:
process.on(\'uncaughtException\')
是否是唯一有效的保护措施?process.on(\'uncaughtException\')
是否也能捕获异步处理过程中的未处理异常?- 是否有一个已经构建好的模块,例如发送电子邮件或写入文件,可以在发生未处理异常情况下利用它?
我会感激任何指针或文章,向我展示处理node.js中未处理异常的常见最佳实践。
下面是关于Node.JS错误处理的总结和整理,包括代码示例和选定博客文章的引用。完整的最佳实践列表在这里可以找到
Node.JS错误处理的最佳实践
第一条:使用Promise处理异步错误
TL;DR:在回调风格中处理异步错误可能是最快进入地狱(也就是嵌套地狱)。您可以给您的代码最好的礼物是使用一个知名的Promise库,它提供了更紧凑和熟悉的代码语法,如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){ ... }); }); }); }); });
博客引用:“我们在Promise方面有问题”(来自博客pouchdb,因“Node Promises”关键字而排名第11)
"...事实上,回调函数做了更加邪恶的事情:在处理的时候我们失去了函数的堆栈,我们通常在程序语言中都能够得到这个。在没有堆栈的情况下编写代码就像在没有制动踏板的情况下驾驶汽车一样:您不会意识到您非常需要它,直到您需要时它却不在那里。Promise的整个目的是在异步操作时返回、抛出以及堆栈的基础语言功能,但您必须知道如何正确使用Promise才能利用它们。"
第2条规则:只使用内置的 Error 对象
简述:经常会看到代码抛出字符串或自定义类型错误,这会使错误处理逻辑和模块间的互操作性变得复杂。无论是拒绝 Promise,抛出异常还是发出错误,使用 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 Error 检查的 API,或者想要了解更多有关错误的信息的 API 的契约。正如我们将看到的,现代 JavaScript 引擎中的 Error 对象具有非常有趣的属性,除了保存传递给构造函数的消息之外…”
第3条规则:区分操作错误和程序员错误
简述:操作错误(例如 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中间件、定时任务、单元测试)在出现错误时都会调用该对象。
否则:不在一个地方处理错误会导致代码重复,可能会导致错误处理不当
代码示例 - 典型的错误流程
//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)“...您可能会在堆栈的几个级别上处理相同的错误。当低级别不能做任何有用的事情,除了将错误传播给其调用者时,就会发生这种情况,后者将错误传播给其调用者,依此类推。通常,只有最高级别的调用者知道适当的响应是什么,无论是重试操作,向用户报告错误还是其他操作。但是,这并不意味着您应该尝试将所有错误报告给单个顶级回调,因为该回调本身无法确定错误发生的上下文”
第5点:使用Swagger文档API错误
简而言之:让API调用者知道可能返回哪些错误,以便他们可以谨慎处理而不会崩溃。这通常使用REST API文档框架(如Swagger)完成
否则: API客户端可能会决定崩溃并重新启动,仅因为它收到了无法理解的错误。注意:您的API的调用者可能是您(在微服务环境中非常典型)
博客引用:“您必须告诉调用者可能发生的错误”
(来自博客Joyent,关键词“Node.JS日志记录”排名第1)…我们已经讨论了如何处理错误,但是当您编写新函数时,如何将错误传递给调用您的函数的代码呢?…如果您不知道可能发生什么错误,或者不知道它们的含义,那么除非偶然,否则您的程序将无法正确运行。因此,如果您要编写新函数,就必须告诉调用者可能会发生什么错误,以及它们的含义
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。
否则: 如果你没有使用查询工具或者一个好的日志查看器,那么你可能需要手动查看混乱的文本文件或通过控制台日志信息,这会让你工作到很晚
代码示例- 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. 可以配置多个目标流。例如,你可以将跟踪日志写入一个文件,但当遇到错误时,将其写入同一文件,然后写入错误文件并同时发送电子邮件...
Number8:使用 APM 产品发现错误和停机时间
简短概括: 监测和性能产品(也称 APM)可以主动评估你的代码库或 API,以便自动突出显示你所忽略的错误、崩溃和缓慢的部分。
否则: 你可能需要花费很大的努力来测量 API 的性能和停机时间,很可能你永远不会意识到哪些是实际情况下最慢的代码部分以及它们如何影响用户体验。
博客引用:"APM 产品细分"(来自 Yoni Goldberg 博客)
"...APM 产品包括化为三个主要部分:1. 网站或 API 监控 - 外部服务通过 HTTP 请求不断监测监视正常运行时间和性能。可在几分钟内设置。以下是几个精选产品:Pingdom、Uptime Robot、New Relic
2. 代码内嵌 - 这个产品系列需要在应用程序中嵌入代理以获得慢代码检测、异常统计、性能监控等功能。以下是几个精选的竞争对手:New Relic,App Dynamics。
3. 运营智能仪表板 - 这些产品专注于为运维团队提供指标和策划内容,以便轻松掌握应用程序的性能。通常包括聚合多个信息来源(应用程序日志,数据库日志,服务器日志等)和上游仪表板设计工作。以下是几个精选的竞争对手:Datadog,Splunk。"上面是一个缩短版本 - 点击这里查看更多的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 })
-
如果我们知道错误发生在同步代码的哪里,但由于某些原因无法使用域(可能是node的旧版本),我们可以使用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捕获不想要的执行。在允许 proper threading 而不是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