文档章节

NodeJS错误处理最佳实践(1)

J
 JasonWP
发布于 2015/04/16 20:01
字数 4507
阅读 4
收藏 0

NodeJS的错误处理让人痛苦,在很长的一段时间里,大量的错误被放任不管。但是要想建立一个健壮的Node.js程序就必须正确的处理这些错误,而且这并不难学。如果你实在没有耐心,那就直接绕过长篇大论跳到“总结”部分吧。【原文

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

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

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

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

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

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

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

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

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

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

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

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

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

  • 总结:全文至此的观点总结。

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

背景

本文假设:

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

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

function myApiFunc(callback) { /*  * This pattern does NOT work!  */ try {   doSomeAsynchronousOperation(function (err) {     if (err)       throw (err);     /* continue as normal */   }); } catch (ex) {   callback(ex); } }

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

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

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

throw new Error('something bad happened');

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

callback(new Error('something bad happened'));

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

操作失败和程序员的失误

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

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

  • 连接不到服务器

  • 无法解析主机名

  • 无效的用户输入

  • 请求超时

  • 服务器返回500

  • 套接字被挂起

  • 系统内存不足

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

  • 读取 undefined 的一个属性

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

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

  • 该传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,你也可以用堆栈信息和日志来开始处理问题。

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

本文作者系OneAPM工程师王龑,想阅读更多文章,请访问OneAPM官方技术博客

© 著作权归作者所有

J
粉丝 0
博文 6
码字总数 16822
作品 0
海淀
私信 提问
Node.js 的后期诊断和调试

在你希望判断出你的 Node.js 应用在生产环境中发生了什么错误时,后期诊断和调试就显得尤为重要了。 这里我们探讨 node-report 这个核心项目,用来帮助我们进行后期诊断和调试。 Node.js At...

TanJx
2017/06/21
1K
2
如何选择正确的Node框架:Express,Koa还是Hapi?

简介 Node.js是10年前首次推出的,目前它已经成为世界上最大的开源项目,在GitHub上有+59,000颗星,下载次数超过10亿。流行度快速增长的部分原因是Node.js允许开发人员在应用程序的客户端和服...

一二三OTT
04/24
0
0
【全开源+免费更新】doodoo.js快速入门教程

简介 Doodoo.js -- 中文最佳实践Node.js快速开发框架。支持Koa.js, Express.js中间件,支持模块机制,插件机制,钩子机制,让开发 Node.js 项目更加简单、高效、灵活。 特性 支持koa全部中间...

doodooke
2018/12/17
0
0
如何基于Node.JS开发个性化全网内容抓取平台

【课程内容】 00课前预习内容 01 搭建http服务 02 Express基础 03 Express中的MVC 04 MongoDB实操 05 mongoose 06 Node.js异步最佳实践 07 错误处理和日志 08 鉴权 09 爬虫系统初步 10 鉴权实...

飞雪团队
2018/05/19
0
0
如何在 2016 年成为一个更好的 Node.js 开发者

本文主要讨论一些进行Node.js开发的最佳实践和建议,这些建议不仅仅适合开发者, 还适合那些管理与维护Node.js基础架构的工作人员。遵循本文提供的这些建议, 能够让你更好的进行日常的开发工...

oschina
2016/01/20
6.8K
5

没有更多内容

加载失败,请刷新页面

加载更多

偶遇 JDK 1.8 还未修复的 SecureRandom.getInstance("SHA1PRNG") 之 bug

楼主今天兴高采烈的在部署环境,下载 JDK,打包项目,上传至服务器。 配置 JDK ,打包上传项目楼主就不在这里重复了,读者自行解决哈! 1. 启动项目 java -jar xxxx.jar 令楼主没有想到的是:...

Ryan-瑞恩
28分钟前
8
0
【更新】Stimulsoft Reports v2019.3.1发布,新增对OData v4的支持功能

下载Stimulsoft Report.Ultimate v2019.3.1试用版 集所有报表解决方案于一体的综合性平台 Stimulsoft Reports.Ultimate是集所有报表解决方案于一体的综合性平台,拥有在JavaScript、ASP.NET...

xiaochuachua
28分钟前
1
0
JVM源码分析之javaagent原理完全解读

JVM源码分析之javaagent原理完全解读 概述 本文重点讲述javaagent的具体实现,因为它面向的是我们Java程序员,而且agent都是用Java编写的,不需要太多的C/C++编程基础,不过这篇文章里也会讲...

BryceLoski
34分钟前
1
0
git记住密码

git取消记住密码 git config --system --unset credential.helper git记住密码 git config --global credential.helper store...

大灰狼wow
35分钟前
2
0
java 面试知识点笔记(十四)异常体系

问:Error和Exception的区别? ps:Throwable上层是Object Error:程序无法处理的系统错误,编译器不做检查 Exception:程序可以处理的异常,捕获后可能恢复 RuntimeException:不可预知的,...

断风格男丶
38分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部