Nodejs的错误处理

本文将回答几个常见的问题:

  • function中出现错误,该何时throw error或通过callback回调,或使用eventEmitter触发?

  • Function的参数应该满足什么?是否需要校验参数的类型,或者满足其他限制如非null、非负、是否是

    ip地址。

  • function不接受传递的参数值,该如何处理?应该throw 抛出异常还是把error传递给回调函数。

  • 能以编程方式区分不同类型的错误吗?

  • 如何提供更多关于错误的描述信息,让调用者更好的处理这个错误?

  • 怎么处理异常?该用try/catchdomains还是其他方式?

 

本文分为几个部分

  • 阅读背景:你需要了解的一些相关知识

  • 运行错误VS程序错误:介绍两种基础的错误类型

  • 函数开发的模式:在function中处理错误的一般原则

  • 开发新function的具体建议:如对函数进行详细的注释文档

  • 总结:概括全文内容

  • 附录:Error对象的常用属性列表

 

阅读背景

  阅读本文的前提

  •  熟悉exception机制,如在javascriptjavaPythonc++或其他相似语言,知道如何抛出

和捕获异常

  •  熟悉node编程。知道node的异步操作和完成异步操作后的callback回调函数模式。

    了解node中三种主要传递error的方式:

    1.Throw一个error,即抛出异常

    2.把error传递给回调函数,让回调函数去处理错误和异常操作的结果

    3.触发EventEmitter的“error”事件

  •  最后,应该掌握javascript(特别是node)中error和exception之间的区别。

    error是一个Error的实例。Error可以被传递给其他函数或者抛出,当Error被抛出时,它就变

    为一个异常。这里有一个Error作为异常的例子:

    throw new Error('something bad happened');

    除此之外,你也可以使用一个Error,不抛出。

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

    这在node中更为常见,因为大部分错误传递是异步的。实际上,唯一能够使用try/catch的情

况是JSON.parse和其他需要验证输入正确性的函数。在同步函数中需要使用catch捕获Error的情

况是非常罕见的,这与javaC++和其他语言处理异常情况是有很大区别的。

 

运行错误VS程序错误

    所有的错误可分为两大类:

  •  运行错误:表示在正确程序编码上的运行时问题,这不是程序的bug。通常是一些系统问题

    (如内存溢出,打开过多文件)、系统配置、网络问题等,包括:

    1.连接服务器失败

    2.解析主机失败

    3.无效的用户输入

    4.请求超时

    5.服务器返回500

    6.Socket挂起

    7.系统内存溢出

  • 程序错误:程序编码中的bug,这种问题可以通过修改代码来避免。比如:

    1.试图读取“undefined”属性

    2.调用异步的函数时未定义回调函数

    3.期望Object时传递了一个“String

    4.期望ip地址时传递了一个对象

    人们使用“Error”的术语去概括运行和程序的错误,但是他们之间是有区别的。运行错误是

程序必须要处理的,只要处理了就不在是bug或者严重问题。“file not found”是一个运行错

误,但并不意味着程序是错的,只需要程序在查找文件时,若不存在则先创建文件。与运行错误

相比,程序错误是bug。也许是忘记校验输入有效性、错误的变量名或者其他。这个区别是很重要

的,运行错误作为程序正常运行的一部分,而程序错误则是bug

    有时,你会同时遇到运行和程序错误。如一个http server试图使用undefined变量后崩溃,

这是程序错误,是bug。在http server崩溃时,node发起客户端请求,会有ECONNRESET的错误,

将出现“socket hang-up”,对于客户端,这是运行错误。同理,若未处理“运行错误”本身也

是程序的错误。当程序试图连接服务器但出现ECONNREFUSED错误,而且未注册socket的“Error

事件,使程序崩溃了就是程序错误。所以连接失败是运行错误,但未处理这个错误就是程序错

误了。理解运行错误和程序错误的区别,是搞清楚如何传递和处理错误的基础,在阅读下面时确

定你已经理解他们的区别。

 

   运行错误的处理

    就像性能和安全问题一样,错误处理在程序中并不是一开始就很完善。不可能用一部分程序

就处理所有的错误,也不可能通过提高部分性能来提高整体的性能。任何代码都可能会出错(打

开文件、连接服务器、启动子进程等等),必须事先考虑失败时会发生什么。包括考虑如何失败

的,该作出什么响应,因为错误的影响和响应都依赖具体的错误。

    当内部把错误传递给外部调用者而未对错误进行有效的处理,最终在多个层次的stack中可能

处理同样的错误。通常,只有最外层的调用者才知道对应错误的响应是什么,是否需要重新执行

操作、或者把错误报告给用户。这并不是说把所有的错误都传递给最外层调用者,因为回调函数

本身不知道错误发生的上下文,操作的哪部分已完成,哪些是失败的。

 

    对于所有错误,可以这样处理:

  • 直接处理错误:有时,很明显必须去处理一个错误。假设你在打开log文件操作时遇到ENOENT

错误,也许是程序首次在该系统运行,你只需要先创建一个log文件就可以避免这个问题。还有一

个例子是操作一个持久化的连接(如数据库),遇到“socket hang-up”错误。这意味着远程服

务器或网络断开,然而这经常是瞬间的,只要重新连接就不在发生这个错误了。

  • 把错误反馈给客户端:如果不知道该如何处理这个错误,最简单的就是终止所有正在执行的

操作,清空已经操作的结果,然后把错误反馈给客户端。当引起错误的因素不会很快发现改变,

如用户给你一个无效的json对象,重新试着转换它是无意义的,所以把这个错误反馈给用户是不

错的方式。

  • 重试操作:如果错误来源于网络或远程服务,那么重新执行操作是不错的选择。如远程服务

503,也许几秒后重试下操作就不会了。如果要重试,应该在注释文档中标明重试的次数,重

的间隔时间。同时记住要防止一直在重试某个操作。

  • 记录错误:有时,除了记录错误的信息日志,做其他任何事情都是徒劳,即不处理错误。

 

   程序错误的处理

    如错误的变量名称引起的程序错误,是无法通过更多的代码(异常处理机制)来处理这种错

误。如果可以通过更多代码处理错误,只需在出错的代码处替换错误的代码。

    有人建议在程序出错后进行恢复操作,即允许失败,但继续持有请求。这是不推荐的方式。

在原始代码中若没有考虑到程序错误,那么能确定这错误不影响其他请求吗?如果其他请求共用

某个状态(一个服务、socket、数据库连接池等),那么可能使其他请求也会失败。

    一个典型例子是一个REST server,某个请求抛出ReferenceError。还会继续导致一系列的

以追踪的bug。如:

    1.一些共享的变量也许是nullundefined或无效的

    2.数据库连接泄露,减少了可以并行处理的数量

    3.数据库连接的事务未关闭

    4.数据库连接处于认证状态,并用于后续连接

    5.Socket未关闭

    6.无效内存引用,导致内存泄露

    处理程序错误最好的方式是立即使程序崩溃,同时程序可以进行自动重启操作。程序崩溃重

启带来的影响是连接的用户会临时中断,但显而易见,这不是我们所说的系统或网络故障,而是

程序中的bug。由于系统崩溃重启而导致用户连接经常断开,显然是代码错误引起,那么要重点处

理使系统崩溃的bug,而不能去避免系统崩溃。调试这些问题的最好方式是使用dump处理异常。在

GUN/LINUXillumos-based系统中,不仅可以跟踪程序堆栈,也可以查看函数参数值和

javascript Object,甚至是闭包中的引用。

    最后,请记住服务端的程序错误,在客户端看来是一个运行错误。客户端也必须处理程序崩

溃和网络异常的情况。这不仅仅停留在理论当中,在实际系统中也是经常会发生的。

 

函数开发的模式

    上面讨论了如何处理错误,但当开发一个新函数时,如何把错误传递给外层调用者呢?

    最重要的一件事是写好函数注释文档,包括函数的功能、参数、返回值、可能的错误。如果

不知道新函数会出现什么错误或不知道错误是什么意思,那么新函数很难被正确的使用。所以在

开发新函数时,必须明确告诉外层调用者可能发生什么错误,错误的处理方式等。

  ThrowCallback、还是EventEmitter这三种方式可以把错误传递给外层调用者

  • Throw 同步的传递错误,即和外层调用者在同一上下文中处理错误。如果调用者使用try/

catch,那么将捕获这个错误,否则程序崩溃。

  • Callback回调函数是异步传递错误的最常用的方式。当异步操作完成后调用Callback,一般

callback(err, result)errresult中只有一个是非空的,这取决于操作是否成功。如成功

callback(null, result),否则callback(err)

  • EventEmitter 用于更复杂的情况,Callback将不在适用。由于function本身也是EventEmitter

象,外层调用者可以监听所以调用函数的error事件。如下情况:

    1.当一个复杂操作有多个错误类型或结果的情况。如假设一个操作请求查询多个数据库记录

,对每个结果到达后进行操作,而不是等所有结果都到达后才操作。对于每条记录,function

以返回一个EventEmitter并触发row事件,最后所有结果完成后触发一个end事件,或发生错误时

触发error事件。

    2.一个对象表示复杂的状态机,可能有很多异步的事件。如socket是一个EventEmitter

象,能触发connectendtimeoutdrainclose事件。当socket发生这些事件时很可能触发

error事件。有一点是只需关注error触发事件,不用管其他事件是否触发、其他事件是否同时

可见、触发顺序如何、socket最终是否关闭。

  何时使用throwCallbackevent emitter?取决于两个方面

    1.是运行错误还是程序错误

    2.函数本身是同步还是异步

    对于大部分异步函数的运行错误,使用Callback作为参数,然后把错误传递给Callback,这

是很好的方式,应用广泛,比如nodefs模块。

    对于常见的同步函数的运行错误,如JSON.parse。应该同步的传递error,可以抛出或返回

    对于一个函数,如果所有的运行错误都可以异步传递,那么应该使用异步传递。某种情况

下,也许可以马上判断请求会失败,但不是由程序错误引起。而且函数缓存了最近请求的结果,

该错误就在缓存中,即使你可以直接返回给调用者,你也应该异步的传递错误。

    一般情况下,一个函数可以同步传递或异步传递 运行错误,但不应该同时出现两种方式。

即可以通过Callbacktry/catch来处理错误,但不应该是同时使用。使用那种方式取决于函数

如何传递错误,并且应该在函数文档注释中标明。

    在这忽略了程序错误。细想下,这些总是一种错误。他们通常可以使用类型校验在函数的开

来识别。如外层调用,未传递Callback参数给被调用的函数,此时应该马上抛出这个错误。

建议在函数开始时就对参数进行类型校验。

    虽然建议程序错误不应该被处理,但这个建议并不影响外层调用者要使用try/catch

Callback(或Event emitter)去处理可能的错误。

    这里概况下一些node核心库中的部分函数

函数

函数类型

错误

错误类型

如何传递

调用者处理

Fs.stat

Asynchronous

File not found

Operational

Callback

Handle callback   error

JSON.parse

Sysnchronous

Bad user input

Operational

Throw

Try/catch

Fs.stat

Asynchronous

Null for   filename

Programmer

Throw

None(crash)

 

   无效输入是程序错误还是运行错误?

    该如何判断哪些是程序错误,哪些又是运行错误?其实很简单,取决于函数需要那种类型的

参数并如何解析参数。假设传递的参数和函数注释文档的不一致,那么就是程序错误。如果输入

的参数符合定义但函数现在无法处理,那么就是运行错误。

    必须判断该如何按照想要的去严格定义函数参数,这里有些建议。来个具体的例子,有个函

connect,定义了一个ip addressCallback参数。假设用户传了一个明显无效的ip address

如‘bob’,此时,有以下处理方式:

  •  注释文档标明函数只接受有效的IPv4地址的字符串,如果参数为‘bob’则抛出异常。强烈

推荐这种方式。

  • 注释文档标明函数接受所有字符串。如果参数为‘bob’,触发异步的错误,提示不能连接到

ip 地址‘bob’。

    这两种可作为运行错误和程序错误的参考,决定输入该作为程序错误还是运行错误。一般而

言,用户输入验证的条件是非常宽松的。如Date.parse接受多种输入,这也是问题的关键。因为

对于大多数函数,输入条件是比较严格的。函数越对输入越宽松,出错的可能也越多,所以推荐

使用更严格的输入校验。宽松的条件有时不仅没有使调用者更专心于需要的地方,反而浪费时间

去调试这个函数。此外,版本升级时可能会减少函数的校验限制,但引起更多猜测用户意图而导

致更多bug,在不破坏兼容性的条件下,很难修复这些bug。所以,如果一个值是无效的,应该在

注释文档中标明这是不允许的,一旦发现就抛出错误。只要不符合注释文档中注明条件,这种错

误就是程序的错误,而不是运行错误。这样就可以把bug造成的影响降至最低,同时提供了有用

的错误信息,便于调用者去调试。

    Domainsprocess.on(‘uncaughtException’)是什么?

    运行错误总是能通过明确的机制去处理:捕获异常、在回调函数中处理或通过EventEmitter

触发error事件等。Domainsprocess‘uncaughtException’事件主要用来处理来自意料之外

的程序错误,这是不推荐的处理方式。


开发新函数的具体建议

  1.在上面讨论了很多原则,现在我们来具体说说:

  • 明确函数内容,做好注释文档,内容如下

  • 函数需要什么参数

  • 每个参数的类型

  • 参数是否还要满足其他条件

  • 若不满足上面的那么就是程序错误,应该立即抛出错误

  • 其他也想在注释文档标明的:

  • 调用者应该考虑哪些运行错误

  • 如何处理这些运行错误(是被抛出、传递给回调还是通过Event Emitter触发)

  • 返回值

  2.对于所有错误使用Error对象或者其子类

    所有的错误应该使用Error对象或者子类,并提供名称和Message属性,stack也可以。

  3.通过Errorname属性,以程序的方式区分不同类型的错误

    当判断是何种错误时,可以使用name属性。也可以重用内置在javascript的名称如

RangeError”、TypeError。对于HTTP Error,通常使用RFC-given状态文本去命名错误,像

BadRequestErrorServiceUnavailableError。不必所有的错误都去创建一个新的名称。不必

细分InvalidHostnameError, InvalidIpAddressError还是InvalidDnsServerError这些无效值

引起的错误,只需一个InvalidArgumentError,说明什么错误就可以了。

  4.包装Error对象,提供扩展属性,描述具体的错误细节

    例如,一个参数是无效的,那么在Error对象里设置对应的属性,名称为参数的名称,值为

数的值。若不能连接到服务器,那么可以使用remoteIp。如果出现系统错误,使用syscall

属性表示syscall失败,同时使用errno属性表示system errno。查看附录了解更多的属性名

称。    

  以下是常用的属性

    Name:用来区分不同类型的Error

    Message:具有可读的,足够完善的错误信息,以便调用者能够更好的理解它。假如传递一个

底层堆栈的错误信息,应该包装下这个错误,添加一些信息解释正在执行的操作等。

    Stack:一般不能修改它。V8只有在明确读取stack属性时才会获取它。如果读取它的属性仅

仅是为了包装扩展它,那么这是增加额外的负担,即使调用者并不需要它。

 

  5.如果传递底层错误给外层调用者,建议包装下错误对象

    如,异步函数funa调用异步函数funb,然后funb触发Error,希望funa也触发同样的Error

这种情况下,建议包装Error,而不是直接返回。包装后,扩展了底层错误对象,让其包含更多

有用的当前上下文信息。Verror模块提供了简单的方式去实现这个。比如,有个fetchconfig

数,从远程数据库获取配置文件。当系统启动时调用这个函数,整个启动路径如下:

        1.加载配置

         1.1连接数据库

            1.1.1解析数据库服务器的DNS主机

            1.1.2建立TCP连接

            1.1.3数据库权限验证

         1.2数据库请求

         1.3解析数据库响应

         1.4加载配置

        2.开始处理请求

    假设运行时,在数据库连接时出错了。问题出在“建立TCP连接”失败,每个层级都会传递错

误给调用者。首次未包装Error时,将得到以下错误信息:

    Myserver: Error: connect ECONNREFUSED

    很明显,这对于调用者没什么帮助

    再次,如果每个层级都包装这个错误对象,将得到更多的信息:

    myserver: failed to start up: failed to load configuration: failed to connect to

    database server: failed to connect to 127.0.0.1 port 1234: connect ECONNREFUSED

    也许你想跳过一些层级的包装,获取更精简的信息:

    myserver: failed to load configuration: connection refused from database at

127.0.0.1 port 1234.

    如果要包装错误对象,这里有些注意事项:

  • 保留原有错误对象的完整性,保证调用者可以直接从它获取一些信息

  • 可以继续使用同样的name属性,也可以使用更有代表性的name属性。比如底层的错误对于node

只是纯粹的错误对象,而对于step 1(加载配置)是初始化错误InitializationError

  • 保持所有原始错误对象的属性。适当的扩展Message属性。浅复制所有的属性如syscall

errno等。除了nameMessagestack,复制其他所有属性。不要对stack进行操作,因为即使只

是读取它都是要很大代价的。


总 结

    1.学会区分(可预期的、无法避免的)运行错误和程序错误。

    2.运行错误可以、应该被处理。程序错误不应该被处理或者即使可以恢复过来,但这会导致

这个问题更难调试。

    3.函数应该同步或异步传递出错误对象,但不能同时使用两种方式。调用者可以使用

try/catchCallback去处理错误,但不能同时使用。一般来说,使用throw抛出异常,调用者使

try/catch是相当少见的。因为在node中对于同步函数出现运行错误是很少见的。(主要的例

子就是需要输入验证的函数,像JSON.parse)。

    4.当开发新函数时,注释文档要清楚标明函数所接受的参数类型,限制条件等,列明可能发

生的运行错误(如解析主机失败,连接服务器失败等),这些错误会如何传递给调用者(同步使

throw,或异步使用CallbackEvent emitter

    5.未传递参数值或无效的参数值是程序错误,发生时应该总是throw这个错误。只要传递的参

数值不符合函数所期望的,那么就是属于程序错误。

    6.错误传递时,使用标准的Error对象和属性。可以添加一些其他的有用信息属性。尽可能的

使用采用的属性名称如附录。

 

附录:Error对象常用属性

强烈建议和Error对象的属性保持一致。有用的信息应该尽可能的详细。

属性名称

用途

localHostname

本地DNS主机名称

localIp

本地ip地址

localPort

本地端口

remoteHostname

远程DNS主机名称

remoteIp

远程ip地址

remotePort

远程主机端口

path

路径

srcpath

原路径

Dstpath

目标路径

hostname

DNS主机名称

Ip

Ip地址

propertyName

属性名称

propertyValue

属性值

syscall

系统调用失败的名称

Errno

Errno的值(如"ENOENT"

 


你可能感兴趣的:(异常处理,nodejs)