Javascript的Nodejs的程序错误:回调出错必须为函数怎么解决

这篇文章作者David Herron过去是Java鼓吹手现茬感觉自己明白过来了,推崇JavaScript了以下原文大意:

在Sun公司的JavaSE团队工作了10多年的人,还在为了用Java字节码实例化一个抽象接口一直拼命到最后一ロ气对于我这位前JavaSE团队成员来说,2011年学习(一个现已失效的网站)上发布博客每周写1-2次,讨论Java生态系统中的事件为期6年,一个重要的话題是保护Java不受那些预测Java死亡的人的影响

杜克奖颁发给了那些超越一切的员工。我是在运行Mustang回归竞赛(bug发现竞赛)和Java1.6版本的同时获得这一成绩嘚

我在这里的目的是解释为什么我这个Java死对头会变成这Node.js/JavaScript拥护者的。

我并没有完全脱离Java在过去3年中,我编写了大量的Java/Spring/Hibernate代码我非常喜欢峩的工作,我在太阳能行业工作做了一些令人心旷神怡的事情,比如用JAVA编写有关千瓦时?-?编码的数据库查询但Java已经失去了光彩。

两姩的Spring编码给出了一个非常清楚的教训:对复杂性进行样板化模式化框架化并不能产生简单性它只会产生更多的复杂性。

Java已经成为一种负擔用Node.js编写代码是充满喜悦的

一些工具或对象是设计师花了多年时间磨练和完善的结果。他们尝试了不同的想法删除了不必要的属性,朂终得到了一个具有正确属性的对象这些对象通常具有一种强大的简单性,非常吸引人Java不是那种系统。

Spring是基于Java开发Web应用程序的流行框架Spring的核心用途,特别是SpringBoot是一个易于使用的预配置JavaEE堆栈。Spring程序员不需要连接所有的servlet、数据持久性、应用服务器等谁知道还有什么,这樣才能得到一个完整的系统相反,Spring负责所有这些细节而你则专注于编码。例如JPARepository类合成数据库查询方法的名称,如“findUserByFirstName”?-?-你不编写任何代码只需将以这种方式命名的方法添加到Repository定义中,Spring将处理其余的方法

这是一个伟大的故事,一个美好的经历但是最后你会发现咜不是。

PersistentObjectException错误时关于传递到持久化的独立实体错误?这需要几天时间才能找出原因这可能是过于简化导致?,这意味着到达REST端点的JSON必須是带有值的ID字段Hibernate过于简单,但需要控制ID值否则抛出这个令人困惑的异常。有数千条同样令人困惑和迟钝的异常消息对于Spring堆栈中的┅个子系统又一个子系统,它就像一个死对头坐在那里等待你犯最微小的错误然后它用一个应用程序崩溃的异常向你猛扑过来。

然后是巨大的堆栈日志记录他们可持续显示几个屏幕,里面充满了抽象的方法Spring显然正在制定实现代码所需的配置。这个抽象级别显然需要相當多的逻辑来查找所有内容并执行请求很长的堆栈跟踪并不一定是坏的。相反它指出了一个症状:内存/性能开销是多少?

当程序员编寫零代码时“findUserByFirstName”将如何执行?该框架必须解析方法名猜测程序员的意图,构造类似抽象语法树的内容生成一些SQL等等。这一切的开销昰多少这样编码器就不用编码了?

在经历了几十次之后花了几个星期的时间去学习你不应该学到的奥秘,你可能会得出和我一样的结論:把复杂写在纸上隐藏起来并不能产生简单它只会产生更多的复杂性。

“兼容性问题”是一个非常酷的口号意味着Java平台的关键价值主张是完全向后兼容。我们把这件事太当回事了把它涂在像自己的T恤上。当然这种程度的兼容性维护可能是个累赘,有时逃避那些不洅有用的老方法反而是有用的

另一方面,Node.js则是…

在Spring和JavaEE极其复杂的地方,Node.js是新鲜空气首先是用于开发核心Node.js平台的设计美学经验Ryan Dahl,为一個重量级的复杂系统使用线程他寻找了一些不同的东西,花了几年时间磨练和完善了一套核心理想成就了Node.js。结果是一个轻量级的系统、一个单一的执行线程、巧妙地使用JavaScript匿名函数进行异步回调出错以及一个巧妙实现异步性的运行库。可以使用事件传递到回调出错函数實现高吞吐量的事件处理

还有JavaScript语言本身。JavaScript程序员似乎有一种删除样板的审美观这样程序员的意图就能清晰地显现出来。

Java和JavaScript之间对比的┅个例子是监听器函数的实现在java,侦听器需要创建抽象接口类的具体实例这需要大量的言语来掩盖正在发生的事情。程序员的意图怎麼能在样板的面纱后面被看到呢

在JavaScript中,可以使用简单的匿名函数?-?闭包你不需要搜索正确的抽象接口,相反你只需编写所需的代碼,而不需要过多的语句

另一种学习:大多数编程语言模糊了程序员的意图,使理解代码变得更加困难

这一点也涉及到Node.js?-?,但是我們必须指出一个警告:回调出错地狱解决办法有时伴随着带入他们自己的问题。

在JavaScript中异步编码长期以来一直存在两个问题,一个是Node.js中所谓的“回调出错地狱”;很容易陷入深度嵌套回调出错函数的陷阱在这种情况下,每一层嵌套都会使代码变得复杂从而使错误和结果处理变得更加困难。

一个相关的问题是JavaScript语言没有帮助程序员正确地表达异步执行出现了几个库,它们承诺简化异步执行这是另一个鼡纸张掩盖复杂性的例子,创造了更多的复杂性


        

这个示例应用程序是对unix的cat命令的简单模仿 。异步库async在简化异步执行的排序方面非常出色但是它的使用需要一堆样板代码来掩盖程序员的意图。

我们这里需要有一个循环但是没有作为循环编写的,也不使用自然循环结构此外,错误和结果不方便地被困在回调出错函数中在ES 功能加入Node.js之前,这是我们所能做的最好的


        

使用异步/等待(asnc/await)功能对前面示例进行了重寫。他们有相同的异步结构但是使用普通循环结构编写的。错误和结果是以自然的方式展现它更容易阅读,代码理解,主动表达了程序员的意图

唯一的毛病是process.stdout.写不提供承诺接口,因此如果不使用Promise就不能在异步函数中干净地使用。

调用地狱的问题并没有通过掩盖复雜性来解决相反,语言和范式的改变既解决了问题也解决了临时解决方案强加给我们的过度言辞,带着异步我们的代码变得更漂亮了

假定通过定义良好的类型和接口来实现清晰

我作为一个死气沉沉的Java倡导者曾经强调的一句教条是:严格的类型检查可以编写巨大的应用程序。当时的规范是开发单一系统(没有微服务没有Dcoker等等)。因为Java有严格的类型检查所以Java编译器通过防止编译糟糕的代码来帮助你避免许哆类型的错误(?-?)。

相比之下JavaScript的打字方式很松散。

理论是显而易见的:程序员无法确定他们收到了什么样的对象那么程序员如何才能知道该做什么呢?

Java中在严格输入的另一面却是需要更多的样板程序员不断地进行打字或努力工作,以确保一切都是正确的编码器花费時间使用更多的样板以极高的精度编译,希望用更少时间能捕捉到和纠正早期的错误

这个问题是如此之大,以致于必使用大型复杂IDE,一个簡单的程序员编辑器是不够的让Java程序员保持理智(除了披萨)的唯一方法是下拉显示对象上的可用字段,描述方法参数帮助构造类,协助偅构以及Eclipse、NetBeans和IntelliJ提供的所有其他工具。

别让我对Maven动手动脚真是个可怕的工具。

在JavaScript中变量类型不会被声明,类型转换通常不会被使用等等。因此代码读起来更清晰,但也存在未明编码错误的风险

这一点是Java好处还是坏处?取决于你的观点十年前,我的看法是通过獲得更多的确定性,这种间接费用是值得的;我今天的观点是天哪这需要花费很多工作,而且用JavaScript的方式做事情要容易得多

用容易测试嘚小模块清除bug

js鼓励程序员将程序划分为小单元,即模块这似乎是一件小事,但它在一定程度上解决了刚刚提到的问题

所有这些都有助於Node.js模块更容易测试并具有定义良好的范围。

担心JavaScript的心理在于如果缺少严格的类型检查,代码很容易出错但是,在一个边界清晰的小聚焦模块中受影响代码的范围主要限于该模块。这使得大多数关注保持小而安全地安置在该模块的边界内

解决松耦合问题的另一个解决方案是增加测试。

你必须将一些效率提高(编写JavaScript代码更容易)才能有时间用于增加测试你的测试机制必须捕获编译器可能捕获的错误。你要洎己测试你的代码不是吗?

对于那些想要在JavaScript中实现静态检查类型的人请看TypeScript。我没有用过那种语言但听过一些很棒的话。它与JavaScript直接兼嫆并添加了有用的类型检查和其他特性。

我一想到Maven就会中风只是脑子不清醒,不能写出任何关于Maven的东西据推测,一个人要么爱Maven要麼鄙视它,而且没有中间立场

一个问题是Java生态系统没有一个具有凝聚力的包管理系统。Maven包存在并运行得相当好而且据说也在Gradle中工作。泹是它并不像Node.js的包管理系统那样有用/可用/强大

在Node.js世界中,有两个优秀的包管理系统紧密地协同工作起初,NPM和NPM存储库是唯一这样的工具

有了NPM,我们就有了一个很好的描述包依赖关系的模式依赖项可以是严格的(确切地说是1.2.3版本),也可以是通过几个松散级别指定的直到“*”,这意味着最新版本.js社区已经将成千上万的包发布到NPM存储库中。使用来自NPM存储库之外的包与使用NPM存储库中的包一样容易

NPM的存储库鈈仅为Node.js服务,而且还为前端工程师服务以前使用过包管理工具,比如BowerBower已经被废弃了,现在人们发现了所有前端JavaScript库都可以作为NPM包使用許多前端工程师工具链,如Vue.js、CLI和Webpack都是用Node.js编写的。

Node.js的另一个包管理系统SEAR是从NPM存储库中提取其包并使用与NPM相同的配置文件。主要的优点是yarn笁具跑得更快

NPM存储库,无论是用NPM访问还是用yarn访问都是使Node.js如此容易和快乐地使用的强大部分。

这两种语言共同点:编译器将源代码转换為由虚拟机实现执行的字节代码VM通常会进一步将字节码编译成本机代码,并使用各种优化技术

Java和JavaScript都有快速运行的巨大潜力。在Java和Node.js中噭励机制是更快速服务器端代码,在Browser-JavaScript中奖励是更好的客户端应用程序性能,?-?参见关于RichInternet应用程序的下一节

Sun/OracleJDK使用HotSpot,这是一个具有多字節代码编译策略的超级DOOPER虚拟机它的名称来自于检测频繁执行的代码,并在代码节执行越多时应用越来越多的优化HotSpot是高度优化的,并产苼非常快的代码

在JavaScript方面,我们过去常常想:我们怎么能期望运行在浏览器中的JavaScript代码实现任何类型的复杂应用程序当然,在基于浏览器嘚JavaScript中办公文档处理套件是不可能的?今天我正在用GoogleDocs写这篇文章,而且性能很好浏览器-JavaScript性能每年都有飞跃。

由于使用Chrome的V8引擎Node.js直接受益于这一趋势。

一个例子是PeterMarshall的演讲他是一位从事V8开发的Google工程师,他的主要工作是提高V8的性能Marshall专门负责Node.js性能方面的工作。他描述了为什麼V8从曲轴虚拟机切换到Turbofan虚拟机

机器学习是一个涉及很多数学的领域,数据科学家通常使用R或Python包括机器学习在内的几个领域都依赖于快速数值计算。由于各种原因JavaScript在这方面做得很差,但是开发一个标准化的JavaScript数值计算库的工作正在进行中

在另一次演讲中,IBM的ChrisBailey讨论了Node.js的性能和可伸缩性问题特别是在Docker/Kubernetes部署方面。他从一组基准测试开始显示Node.js在I/O吞吐量、应用程序启动时间和内存占用方面的性能明显优于SpringBoot。此外Node.js的逐版本性能正在显著提高,这在一定程度上要归功于V8的改进

在视频中,Bailey说人们不应该在Node.js中运行计算代码其中的“为什么”很重偠,这是因为单线程模型长时间运行的计算会阻止事件的执行。

如果说这些JavaScript改进对应用程序来说还不够那么有两种方法可以直接将原苼代码集成到Node.js中。最简单的方法是使用一个node-gyp的Node.js模块可以处理到原生本地代码模块的链接。

WebAssembly提供了将其他语言编译成运行非常快的JavaScript子集的能力WebAssembly是在JavaScript引擎中运行的可执行代码的可移植格式。

丰富的因特网应用程序(RIA)

十年前软件业在用加速JavaScript引擎来实现富互联网应用程序,这些引擎会使桌面应用程序变得不再重要

Navigator中的Java小程序达成了一项协议,JavaScript语言部分是作为Javaapplet的脚本语言开发的希望服务器端有JavaServlet,客户端有Javaapplet这給了我们在这两种语言上都有相同编程语言的条件。这种情况因为各种原因而并没有发生

十年前,JavaScript开始变得足够强大可以单独实现复雜的应用程序,因此RIA的流行词,RIA应该是杀死了Java作为一个客户端应用程序平台的可能

今天,我们开始看到RIA的想法实现了使用服务器上嘚Node.js,我们现在可以使用Nirvana但是在连接的两端使用JavaScript。

Java作为桌面应用程序平台并没有因为JavaScriptRIA而消亡它的死亡主要是由于SUN公司对客户端技术的忽視,Sun专注于要求快速服务器端性能的企业客户我当时在场,亲眼看到了

真正扼杀applet的是几年前在Java插件和JavaWebStart中发现的一个糟糕的安全漏洞,該bug导致全球范围内的警报停止使用Javaapplet和Webstart应用程序

还可以开发其他类型的Java桌面应用程序,因此NetBeans和EclipseIDE之间的竞争依然存在但是在Java这一领域的工莋是停滞不前的,在开发工具之外很少有基于Java的应用程序。

这个领域的所有兴奋都发生在Reaction、Vue.js和类似的框架中

在本例中,JavaScript和Node.js在很大程度仩赢得了这一点

今天,开发服务器端代码有许多选择我们不再局限于“P语言”(Perl、PHP、Python)和Java,因为还有Node.js、Ruby、Haskell、Go、Rust等等因此,我们有一种尴尬的财富可以享受

关于为什么Java会转向Node.js,很明显我更喜欢用Node.js编写代码时的自由感。Java成了负担对于Node.js来说,没有这样的负担如果我再次受雇于编写Java,我当然会用Java编写因为我被付了工资的。

每个应用程序都有其真实的需求当然,不能因为人们更喜欢Node.js就总是使用Node.js这也是鈈正确的。必须有技术上的理由来选择一种语言或框架而不选择另一种语言或框架

这篇文章会回答NodeJS初学者的若干问題:

  • 我写的函数里什么时候该抛出异常什么时候该传给callback, 什么时候触发EventEmitter等等。

  • 我的函数对参数该做出怎样的假设我应该检查更加具体的約束么?例如参数是否非空是否大于零,是不是看起来像个IP地址等等等。

  • 我该如何处理那些不符合预期的参数我是应该抛出一个异瑺,还是把错误传递给一个callback

  • 我该怎么在程序里区分不同的异常(比如“请求错误”和“服务不可用”)?

  • 我怎么才能提供足够的信息让調用者知晓错误细节

  • 我该怎么处理未预料的出错?我是应该用 try/catch domains 还是其它什么方式呢?

这篇文章可以划分成互相为基础的几个部分:

  • 背景:希望你所具备的知识

  • 操作失败和程序员的失误:介绍两种基本的异常。

  • 编写新函数的实践:关于怎么让函数产生有用报错的基本原則

  • 编写新函数的具体推荐:编写能产生有用报错的、健壮的函数需要的一个检查列表

  • 例子:以connect函数为例的文档和序言。

  • 总结:全文至此嘚观点总结

  • 附录:Error对象属性约定:用标准方式提供一个属性列表,以提供更多信息

  • 你已经熟悉了JavaScript、Java、 Python、 C++ 或者类似的语言中异常的概念,而且你知道抛出异常和捕获异常是什么意思

  • 你熟悉怎么用NodeJS编写代码。你使用异步操作的时候会很自在并能用callback(err,result)模式去完成异步操作。伱得知道下面的代码不能正确处理异常的原因是什么[脚注1]

你还要熟悉三种传递错误的方式: - 作为异常抛出 - 把错误传给一个callback,这个函数正是為了处理异常和处理异步操作返回结果的 - 在EventEmitter上触发一个Error事件。

接下来我们会详细讨论这几种方式这篇文章不假设你知道任何关于domains的知識。

最后你应该知道在JavaScript里,错误和异常是有区别的错误是Error的一个实例。错误被创建并且直接传递给另一个函数或者被抛出如果一个錯误被抛出了那么它就变成了一个异常[脚注2]。举个例子:

但是使用一个错误而不抛出也是可以的

这种用法更常见因为在NodeJS里,大部分的错誤都是异步的实际上,try/catch唯一常用的是在JSON.parse和类似验证用户输入的地方接下来我们会看到,其实很少要捕获一个异步函数里的异常这一點和Java,C++以及其它严重依赖异常的语言很不一样。

操作失败和程序员的失误

把错误分成两大类很有用[脚注3]:

  • 操作失败 是正确编写的程序在運行时产生的错误它并不是程序的Bug,反而经常是其它问题:系统本身(内存不足或者打开文件数过多)系统配置(没有到达远程主机嘚路由),网络问题(端口挂起)远程服务(500错误,连接失败)例子如下:

  • 程序员失误 是程序里的Bug。这些错误往往可以通过修改代码避免它们永远都没法被有效的处理。

  • 调用异步函数没有指定回调出错

  • 该传对象的时候传了一个字符串

  • 该传IP地址的时候传了一个对象

人们紦操作失败和程序员的失误都称为“错误”但其实它们很不一样。操作失败是所有正确的程序应该处理的错误情形只要被妥善处理它們不一定会预示 着Bug或是严重的问题。“文件找不到”是一个操作失败但是它并不一定意味着哪里出错了。它可能只是代表着程序如果想鼡一个文件得事先创建它

与之相反,程序员失误是彻彻底底的Bug这些情形下你会犯错:忘记验证用户输入,敲错了变量名诸如此类。這样的错误根本就没法被处理如果可以,那就意味着你用处理错误的代码代替了出错的代码

这样的区分很重要:操作失败是程序正常操作的一部分。而由程序员的失误则是Bug

有的时候,你会在一个Root问题里同时遇到操作失败和程序员的失误HTTP服务器访问了未定义的变量时奔溃了,这是程序员的失误当前连接着的客户端会在程序崩溃的同时看到一个ECONNRESET错误,在NodeJS里通常会被报成“Socket Hang-up”对客户端来说,这是一个鈈相关的操作失败, 那是因为正确的客户端必须处理服务器宕机或者网络中断的情况

类似的,如果不处理好操作失败, 这本身就是一个失误举个例子,如果程序想要连接服务器但是得到一个ECONNREFUSED错误,而这个程序没有监听套接字上的 error事件,然后程序崩溃了这是程序员的失误。連接断开是操作失败(因为这是任何一个正确的程序在系统的网络或者其它模块出问题时都会经历的)如果它不被正确处理,那它就是┅个失误

理解操作失败和程序员失误的不同, 是搞清怎么传递异常和处理异常的基础。明白了这点再继续往下读

就像性能和安全问题一樣,错误处理并不是可以凭空加到一个没有任何错误处理的程序中的你没有办法在一个集中的地方处理所有的异常,就像你不能在一 个集中的地方解决所有的性能问题你得考虑任何会导致失败的代码(比如打开文件,连接服务器Fork子进程等)可能产生的结果。包括为什麼出错错误背 后的原因。之后会提及但是关键在于错误处理的粒度要细,因为哪里出错和为什么出错决定了影响大小和对策

你可能會发现在栈的某几层不断地处理相同的错误。这是因为底层除了向上层传递错误上层再向它的上层传递错误以外,底层没有做任何有意義的事情通 常,只有顶层的调用者知道正确的应对是什么是重试操作,报告给用户还是其它但是那并不意味着,你应该把所有的错誤全都丢给顶层的回调出错函数因为,顶层 的回调出错函数不知道发生错误的上下文不知道哪些操作已经成功执行,哪些操作实际上夨败了

我们来更具体一些。对于一个给定的错误你可以做这些事情:

  • 直接处理。有的时候该做什么很清楚如果你在尝试打开日志文件嘚时候得到了一个ENOENT错 误,很有可能你是第一次打开这个文件你要做的就是首先创建它。更有意思的例子是你维护着到服务器(比如数據库)的持久连接,然后遇到了一个 “socket hang-up”的异常这通常意味着要么远端要么本地的网络失败了。很多时候这种错误是暂时的所以大部汾情况下你得重新连接来解决问题。(这和接下来 的重试不大一样因为在你得到这个错误的时候不一定有操作正在进行)

  • 把出错扩散到愙户端。如果你不知道怎么处理这个异常最简单的方式就是放弃你正在执行的操作,清理所有 开始的然后把错误传递给客户端。(怎麼传递异常是另外一回事了接下来会讨论)。这种方式适合错误短时间内无法解决的情形比如,用户提交了不正确的 JSON你再解析一次昰没什么帮助的。

  • 重试操作对于那些来自网络和远程服务的错误,有的时候重试操作就可以解决问题比如,远程服务返回了503(服务不鈳用错误)你可能会在几秒种后重试。如果确定要重试你应该清晰的用文档记录下将会多次重试,重试多少次直到失败,以及两次重试嘚间隔 另外,不要每次都假设需要重试如果在栈中很深的地方(比如,被一个客户端调用而那个客户端被另外一个由用户操作的客戶端控制),这种情形下快速失败让 客户端去重试会更好如果栈中的每一层都觉得需要重试,用户最终会等待更长的时间因为每一层嘟没有意识到下层同时也在尝试。

  • 直接崩溃对于那些本不可能发生的错误,或者由程序员失误导致的错误(比如无法连接到同一程序里嘚本地套接字)可以记录一个错误日志然后直接崩溃。其它的比如内存不足这种错误是JavaScript这样的脚本语言无法处理的,崩溃是十分合理嘚(即便如此,在child_process.exec这样的分离的操作里得到ENOMEM错误,或者那些你可以合理处理的错误时你应该考虑这么做)。在你无计可施需要让管悝员做修复的时候你也可以直接崩溃。如果你用光了所有的文件描述符或者没有访问配置文件的权限这种情况下你什么都做不了,只能等某个用户登录系统把东西修好

  • 记录错误,其他什么都不做有的时候你什么都做不了,没有操作可以重试或者放弃没有任何理由崩溃掉应 用程序。举个例子吧你用DNS跟踪了一组远程服务,结果有一个DNS失败了除了记录一条日志并且继续使用剩下的服务以外,你什么嘟做不了但是,你至 少得记录点什么(凡事都有例外如果这种情况每秒发生几千次,而你又没法处理那每次发生都记录可能就不值嘚了,但是要周期性的记录)

(没有办法)处理程序员的失误

对于程序员的失误没有什么好做的。从定义上看一段本该工作的代码坏掉了(比如变量名敲错),你不能用更多的代码再去修复它一旦你这样做了,你就使用错误处理的代码代替了出错的代码

有些人赞成從程序员的失误中恢复,也就是让当前的操作失败但是继续处理请求。这种做法不推荐考虑这样的情况:原始代码里有一个失误是没栲虑到某 种特殊情况。你怎么确定这个问题不会影响其他请求呢如果其它的请求共享了某个状态(服务器,套接字数据库连接池等),有极大的可能其他请求会不正常

典型的例子是REST服务器(比如用Restify搭的),如果有一个请求处理函数抛出了一个ReferenceError(比如变量名打错)。繼续运行下去很有肯能会导致严重的Bug而且极其难发现。例如:

  1. 一些请求间共享的状态可能会被变成nullundefined或者其它无效值,结果就是下一个請求也失败了

  2. 数据库(或其它)连接可能会被泄露,降低了能够并行处理的请求数量最后只剩下几个可用连接会很坏,将导致请求由並行变成串行被处理

  3. 更糟的是, postgres 连接会被留在打开的请求事务里这会导致 postgres “持有”表中某一行的旧值,因为它对这个事务可见这个問题会存在好几周,造成表无限制的增长后续的请求全都被拖慢了,从几毫秒到几分钟[脚注4]虽 然这个问题和 postgres 紧密相关,但是它很好的說明了程序员一个简单的失误会让应用程序陷入一种非常可怕的状态

  4. 连接会停留在已认证的状态,并且被后续的连接使用结果就是在請求里搞错了用户。

  5. 套接字会一直打开着一般情况下 NodeJS 会在一个空闲的套接字上应用两分钟的超时,但这个值可以覆盖这将会泄露一个攵件描述符。如果这种情况不断发生程序会因为用光了所有的文件描述符而强 退。即使不覆盖这个超时时间客户端会挂两分钟直到 “hang-up” 错误的发生。这两分钟的延迟会让问题难于处理和调试

  6. 很多内存引用会被遗留。这会导致泄露进而导致内存耗尽,GC需要的时间增加最后性能急剧下降。这点非常难调试而且很需要技巧与导致造成泄露的失误联系起来。

最好的从失误恢复的方法是立刻崩溃你应该鼡一个restarter 来启动你的程序,在奔溃的时候自动重启如果restarter 准备就绪,崩溃是失误来临时最快的恢复可靠服务的方法

奔溃应用程序唯一的负媔影响是相连的客户端临时被扰乱,但是记住:

  • 从定义上看这些错误属于Bug。我们并不是在讨论正常的系统或是网络错误而是程序里实際存在的Bug。它们应该在线上很罕见并且是调试和修复的最高优先级。

  • 上面讨论的种种情形里请求没有必要一定得成功完成。请求可能荿功完成可能让服务器再次崩溃,可能以某种明显的方式不正确的完成或者以一种很难调试的方式错误的结束了。

  • 在一个完备的分布式系统里客户端必须能够通过重连和重试来处理服务端的错误。不管 NodeJS 应用程序是否被允许崩溃网络和系统的失败已经是一个事实了。

  • 洳果你的线上代码如此频繁地崩溃让连接断开变成了问题那么正真的问题是你的服务器Bug太多了,而不是因为你选择出错就崩溃

如果出現服务器经常崩溃导致客户端频繁掉线的问题,你应该把经历集中在造成服务器崩溃的Bug上把它们变成可捕获的异常,而不是在代码明显囿问题 的情况下尽可能地避免崩溃调试这类问题最好的方法是,把 NodeJS 配置成出现未捕获异常时把内核文件打印出来在 GNU/Linux 或者 基于 illumos 的系统上使用这些内核文件,你不仅查看应用崩溃时的堆栈记录还可以看到传递给函数的参数和其它的 JavaScript 对象,甚至是那些在闭包里引用的变量即使没有配置 code dumps,你也可以用堆栈信息和日志来开始处理问题

最后,记住程序员在服务器端的失误会造成客户端的操作失败还有客户端必须处理好服务器端的奔溃和网络中断。这不只是理论而是实际发生在线上环境里。

我们已经讨论了如何处理异常那么当你在编写新嘚函数的时候,怎么才能向调用者传递错误呢

最最重要的一点是为你的函数写好文档,包括它接受的参数(附上类型和其它约束)返囙值,可能发生的错误以及这些错误意味着什么。如果你不知道会导致什么错误或者不了解错误的含义那你的应用程序正常工作就是┅个巧合。 所以当你编写新的函数的时候,一定要告诉调用者可能发生哪些错误和错误的含义

函数有三种基本的传递错误的模式。

  • throw以哃步的方式传递异常--也就是在函数被调用处的相同的上下文如果调用者(或者调用者的调用者)用了try/catch,则异常可以捕获如果所有的调鼡者都没有用,那么程序通常情况下会崩溃(异常也可能会被domains或者进程级的uncaughtException捕捉到详见下文)。

  • Callback 是最基础的异步传递事件的一种方式鼡户传进来一个函数(callback),之后当某个异步操作完成后调用这个 callback通常 callback 会以callback(err,result)的形式被调用,这种情况下 err和 result必然有一个是非空的,取决于操作是成功还是失败

  • 更复杂的情形是,函数没有用 Callback 而是返回一个 EventEmitter 对象调用者需要监听这个对象的 error事件。这种方式在两种情况下很有用

  • 当你在做一个可能会产生多个错误或多个结果的复杂操作的时候。比如有一个请求一边从数据库取数据一边把数据发送回客户端,而鈈是等待所有的结果一起到达在这个例子里,没有用 callback而是返回了一个 EventEmitter,每个结果会触发一个row 事件当所有结果发送完毕后会触发end事件,出现错误时会触发一个error事件

  • 用在那些具有复杂状态机的对象上,这些对象往往伴随着大量的异步事件例如,一个套接字是一个EventEmitter它鈳能会触发 “connect“,”end“”timeout“,”drain“”close“事件。这样很自然地可以把”error“作为另外一种可以被触发 的事件。在这种情况下清楚知道”error“还有其它事件何时被触发很重要,同时被触发的还有什么事件(例如”close“)触发的顺序,还有套接字 是否在结束的时候处于关闭状態

在大多数情况下,我们会把 callback 和 event emitter 归到同一个“异步错误传递”篮子里如果你有传递异步错误的需要,你通常只要用其中的一种而不是哃时使用

那么,什么时候用throw什么时候用callback,什么时候又用 EventEmitter 呢这取决于两件事:

  • 这是操作失败还是程序员的失误?

  • 这个函数本身是同步嘚还是异步的

直到目前,最常见的例子是在异步函数里发生了操作失败在大多数情况下,你需要写一个以回调出错函数作为参数的函數然后你会把异常传递给这个回调出错函数。这种方式工作的很好并且被广泛使用。例子可参照 NodeJS 的fs模块如果你的场景比上面这个还複杂,那么你可能就得换用 EventEmitter 了不过你也还是在用异步方式传递这个错误。

其次常见的一个例子是像JSON.parse这样的函数同步产生了一个异常对這些函数而言,如果遇到操作失败(比如无效输入)你得用同步的方式传递它。你可以抛出(更加常见)或者返回它

对于给定的函数,如果有一个异步传递的异常那么所有的异常都应该被异步传递。可能有这样的情况请求一到来你就知道它会失败,并且知道不是因為程序员的失误可能的情形是你缓存了返回给最近请求的错误。虽然你知道请求一定失败但是你还是应该用异步的方式传递它。

通用嘚准则就是 你即可以同步传递错误(抛出)也可以异步传递错误(通过传给一个回调出错函数或者触发EventEmitter的 error事件),但是不用同时使用鉯这种方式,用户处理异常的时候可以选择用回调出错函数还是用try/catch但是不需要两种都用。具体用哪一个取决于异常是怎么传递的这点嘚在文档里说明清楚。

差点忘了程序员的失误回忆一下,它们其实是Bug在函数开头通过检查参数的类型(或是其它约束)就可以被立即發现。一个退化的例子是某人调用 了一个异步的函数,但是没有传回调出错函数你应该立刻把这个错抛出,因为程序已经出错而在这個点上最好的调试的机会就是得到一个堆栈信息如果有内核信息就 更好了。

因为程序员的失误永远不应该被处理上面提到的调用者只能用try/catch或者回调出错函数(或者 EventEmitter)其中一种处理异常的准则并没有因为这条意见而改变。如果你想知道更多请见上面的 (不要)处理程序員的失误。

下表以 NodeJS 核心模块的常见函数为例做了一个总结,大致按照每种问题出现的频率来排列:

异步函数里出现操作错误的例子(第┅行)是最常见的在同步函数里发生操作失败(第二行)比较少见,除非是验证用户输入程序员失误(第三行)除非是在开发环境下,否则永远都不应该出现

吐槽:程序员失误还是操作失败?

你怎么知道是程序员的失误还是操作失败呢很简单,你自己来定义并且记茬文档里包括允许什么类型的函数,怎样打断它的执行如果你得到的异常不是文档里能接受的,那就是一个程序员失误如果在文档裏写明接受但是暂时处理不了的,那就是一个操作失败

你得用你的判断力去决定你想做到多严格,但是我们会给你一定的意见具体一些,想象有个函数叫做“connect”它接受一个IP地址和一个回调出错函数作为参数,这个回调出错函数会在成功或者失败的时候被调用现在假設用户传进来一个明显不是IP地址的参数,比如“bob”这个时候你有几种选择:

  • 在文档里写清楚只接受有效的IPV4的地址,当用户传进来“bob”的時候抛出一个异常强烈推荐这种做法。

  • 在文档里写上接受任何string类型的参数如果用户传的是“bob”,触发一个异步错误指明无法连接到“bob”这个IP地址

这两种方式和我们上面提到的关于操作失败和程序员失误的指导原则是一致的。你决定了这样的输入算是程序员的失误还是操作失败通常,用户输入的校验是很松的为了证明这点,可以看Date.parse这 个例子它接受很多类型的输入。但是对于大多数其它函数我们強烈建议你偏向更严格而不是更松。你的程序越是猜测用户的本意(使用隐式的转换无论是 JavaScript语言本身这么做还是有意为之),就越是容噫猜错本意是想让开发者在使用的时候不用更加具体,结果却耗费了人家好几个小时在 Debug上再说了,如果你觉得这是个好主意你也可鉯在未来的版本里让函数不那么严格,但是如果你发现由于猜测用户的意图导致了很多恼人的bug要 修复它的时候想保持兼容性就不大可能叻。

所以如果一个值怎么都不可能是有效的(本该是string却得到一个undefined本该是string类型的IP 但明显不是),你应该在文档里写明是这不允许的并且立刻抛出一个异常只要你在文档里写的清清楚楚,那这就是一个程序员的失误而不是操作失败立即抛出可 以把Bug带来的损失降到最小,并苴保存了开发者可以用来调试这个问题的信息(例如调用堆栈,如果用内核文件还可以得到参数和内存分布)

操作失败总是可以被显礻的机制所处理的:捕获一个异常,在回调出错里处理错误或者处理EventEmitter的“error”事件等等。Domains以及进程级别的‘uncaughtException’主要是用来从未料到的程序錯误恢复的由于上面我们所讨论的原因,这两种方式都不鼓励

我们已经谈论了很多指导原则,现在让我们具体一些

  1. 你的函数做什么嘚很清楚。

这点非常重要每个接口函数的文档都要很清晰的说明: - 预期参数 - 参数的类型 - 参数的额外约束(例如,必须是有效的IP地址)

如果其中有一点不正确或者缺少那就是一个程序员的失误,你应该立刻抛出来

  • 调用者可能会遇到的操作失败(以及它们的name

  • 怎么处理操莋失败(例如是抛出,传给回调出错函数还是被 EventEmitter 发出)

  1. 使用 Error 对象或它的子类,并且实现 Error 的协议

你的所有错误要么使用 Error 类要么使用它的孓类。你应该提供namemessage属性stack也是(注意准确)。

  1. 在程序里通过 Error 的 name 属性区分不同的错误

  1. 用详细的属性来增强 Error 对象。

举个例子如果遇到无效参数,把 propertyName 设成参数的名字把 propertyValue 设成传进来的值。如果无法连到服务器用 remoteIp 属性指明尝试连接到的 IP。如果发生一个系统错误在syscal 属性里设置是哪个系统调用,并把错误代码放到errno属性里具体你可以查看附录,看有哪些样例属性可以用

name:用于在程序里区分众多的错误类型(唎如参数非法和连接失败)

message:一个供人类阅读的错误消息。对可能读到这条消息的人来说这应该已经足够完整如果你从更底层的地方传遞了一个错误,你应该加上一些信息来说明你在做什么怎么包装异常请往下看。

stack:一般来讲不要随意扰乱堆栈信息甚至不要增强它。V8引擎只有在这个属性被读取的时候才会真的去运算以此大幅提高处理异常时候的性能。如果你读完再去增强它结果就会多付出代价,哪怕调用者并不需要堆栈信息

你还应该在错误信息里提供足够的消息,这样调用者不用分析你的错误就可以新建自己的错误它们可能會本地化这个错误信息,也可能想要把大量的错误聚集到一起再或者用不同的方式显示错误信息(比如在网页上的一个表格里,或者高煷显示用户错误输入的字段)

  1. 若果你传递一个底层的错误给调用者,考虑先包装一下

经常会发现一个异步函数funcA调用另外一个异步函数funcB,如果funcB抛出了一个错误希望funcA也抛出一模一样的错误。(请注意第二部分并不总是跟在第一部分之后。有的时候funcA会重新尝试有的时候叒希望funcA忽略错误因为无事可做。但在这里我们只讨论funcA直接返回funcB错误的情况)

在这个例子里,可以考虑包装这个错误而不是直接返回它包装的意思是继续抛出一个包含底层信息的新的异常,并且带上当前层的上下文用 verror 这个包可以很简单的做到这点。

举个例子假设有一個函数叫做 fetchConfig,这个函数会到一个远程的数据库取得服务器的配置你可能会在服务器启动的时候调用这个函数。整个流程看起来是这样的:

1.加载配置 1.1 连接数据库  1.1.1 解析数据库服务器的DNS主机名 1.1.2 建立一个到数据库服务器的TCP连接 1.1.3 向数据库服务器认证 1.2 发送DB请求 1.3 解析返回结果 1.4 加载配置 2 开始处理请求

假设在运行时出了一个问题连接不到数据库服务器如果连接在 1.1.2 的时候因为没有到主机的路由而失败了,每个层都不加处理地嘟把异常向上抛出给调用者你可能会看到这样的异常信息:

另一方面,如果每一层都把下一层返回的异常包装一下你可以得到更多的信息:

你可能会想跳过其中几层的封装来得到一条不那么充满学究气息的消息:

不过话又说回来,报错的时候详细一点总比信息不够要好

如果你决定封装一个异常了,有几件事情要考虑:

  • 保持原有的异常完整不变保证当调用者想要直接用的时候底层的异常还可用。

  • 要么鼡原有的名字要么显示地选择一个更有意义的名字。例如最底层是 NodeJS 报的一个简单的Error,但在步骤1中可以是个 IntializationError (但是如果程序可以通过其它的属性区分,不要觉得有责任取一个新的名字)

  • 保留原错误的所有属性在合适的情况下增强message属性(但是不要在原始的异常上修改)。浅拷贝其它的像是syscallerrno这类的属性。最好是直接拷贝除了 namemessagestack以外的所有属性,而不是硬编码等待拷贝的属性列表不要理会stack,因为即使昰读取它也是相对昂贵的如果调用者想要一个合并后的堆栈,它应该遍历错误原因并打印每一个错误的堆栈

在Joyent,我们使用 verror 这个模块来葑装错误因为它的语法简洁。写这篇文章的时候它还不能支持上面的所有功能,但是会被扩展以期支持

考虑有这样的一个函数,这個函数会异步地连接到一个IPv4地址的TCP端口我们通过例子来看文档怎么写:

 
这个例子在概念上很简单,但是展示了上面我们所谈论的一些建議:
  • 参数类型以及其它一些约束被清晰的文档化。

  • 这个函数对于接受的参数是非常严格的并且会在得到错误参数的时候抛出异常(程序员的失误)。

  • 可能出现的操作失败集合被记录了通过不同的”name“值可以区分不同的异常,而”errno“被用来获得系统错误的详细信息

  • 异瑺被传递的方式也被记录了(通过失败时调用回调出错函数)。

  • 返回的错误有”remoteIp“和”remotePort“字段这样用户就可以定义自己的错误了(比如,一个HTTP客户端的端口号是隐含的)

  • 虽然很明显,但是连接失败后的状态也被清晰的记录了:所有被打开的套接字此时已经被关闭

 
这看起来像是给一个很容易理解的函数写了超过大部分人会写的的超长注释,但大部分函数实际上没有这么容易理解所有建议都应该被有选擇的吸收,如果事情很简单你应该自己做出判断,但是记住:用十分钟把预计发生的记录下来可能之后会为你或其他人节省数个小时
 
  • 學习了怎么区分操作失败,即那些可以被预测的哪怕在正确的程序里也无法避免的错误(例如无法连接到服务器);而程序的Bug则是程序員失误。

  • 操作失败可以被处理,也应当被处理程序员的失误无法被处理或可靠地恢复(本不应该这么做),尝试这么做只会让问题更难调試

  • 一个给定的函数,它处理异常的方式要么是同步(用throw方式)要么是异步的(用callback或者EventEmitter)不会两者兼具。用户可以在回调出错函数里处悝错误也可以使用 try/catch捕获异常 ,但是不能一起用实际上,使用throw并且期望调用者使用 try/catch 是很罕见的因为 NodeJS 里的同步函数通常不会产生运行失敗(主要的例外是类似于JSON.parse的用户输入验证函数)。

  • 在写新函数的时候用文档清楚地记录函数预期的参数,包括它们的类型、是否有其它約束(例如必须是有效的IP地址)可能会发生的合理的操作失败(例如无法解析主机名,连接服务器失败所有的服务器端错误),错误昰怎么传递给调用者的(同步用throw,还是异步用 callback 和 EventEmitter)。

  • 缺少参数或者参数无效是程序员的失误一旦发生总是应该抛出异常。函数的作鍺认为的可接受的参数可能会有一个灰色地带但是如果传递的是一个文档里写明接收的参数以外的东西,那就是一个程序员失误

  • 传递錯误的时候用标准的 Error 类和它标准的属性。尽可能把额外的有用信息放在对应的属性里如果有可能,用约定的属性名(如下)

 
强烈建议伱在发生错误的时候用这些名字来保持和Node核心以及Node插件的一致。这些大部分不会和某个给定的异常对应但是出现疑问的时候,你应该包含任何看起来有用的信息即从编程上也从自定义的错误消息上。【表】
  1. 人们有的时候会这么写代码,他们想要在出现异步错误的时候調用 callback 并把错误作为参数传递他们错误地认为在自己的回调出错函数(传递给 doSomeAsynchronousOperation 的函数)里throw 一个异常,会被外面的catch代码块捕获try/catch和异步函数鈈是这么工作的。回忆一下异步函数的意义就在于被调用的时候myApiFunc函数已经返回了。这意味着try代码块已经退出了这个回调出错函数是由Node矗接调用的,外面并没有try的代码块如果你用这个反模式,结果就是抛出异常的时候程序崩溃了。

  2. 在JavaScript里抛出一个不属于Error的参数从技术仩是可行的,但是应该被避免这样的结果使获得调用堆栈没有可能,代码也无法检查”name“属性或者其它任何能够说明哪里有问题的属性。

  3. 操作失败和程序员的失误这一概念早在NodeJS之前就已经存在存在了不严格地对应者Java里的checked和unchecked异 常,虽然操作失败被认为是无法避免的比洳 OutOfMemeoryError,被归为uncheked异常在C语言里有对应的概念,普通异常处理和使用断言维基百科上关于断言的的文章也有关 于什么时候用断言什么时候用普通的错误处理的类似的解释。

  4. 如果这看起来非常具体那是因为我们在产品环境中遇到这样过这样的问题。这真的很可怕

本文作者系笁程师 王龑,出自OneAPM官方

我要回帖

更多关于 投屏显示auth错误 的文章

 

随机推荐