作者:vienwu
本文内容大部分来自 https://www.joyent.com/node-js/production/design/errors ,原文比较长,感觉也有点啰嗦,所以根据个人理解猜测梳理出本文,如果有错误欢迎指出,谢谢!
很多人其实不是很重视错误处理,但对于构建一个健壮的nodejs应用,错误处理是非常重要的一件事情,希望本文可以给你一些启发。
先抛出几个问题:
应该用哪种方式暴露错误?<code>throw</code>、<code>callback(err, result)</code>、<code>Event Emitter</code>或者其他方式?
如何假设函数的参数?是否应该检测类型正确?非null,IP,QQ号码?
函数参数不符合预期该怎么处理?
应该如何区分不同类型的错误?例如<code>Bad Request</code>、 <code>Service Unavailable</code>。
应该如何提供有用的错误信息?
应该如何捕获错误?使用<code>try/catch</code>,还是<code>domains</code>或者其他方式?
关于<code>Error</code>、<code>throw</code>、<code>try...catch</code>的一些基础知识链接
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch
node.js v7.2.0 <code>domain</code>、<code>process</code>
https://nodejs.org/api/domain.html
https://nodejs.org/api/process.html
verror模块: rich JavaScript errors
https://github.com/joyent/node-verror
抛出错误的几种方式:
捕获错误
一般来说,我们将错误简单的分为两种类型:操作错误、编码错误。
对于有经验的人来说,写代码的时候都会处理一些常见的操作错误,例如<code>JSON.parse</code>总是会和<code>try...catch</code>一起,例如网络故障、远程服务器返回500等。这些错误并非bug。
对于程序来说,另外一种错误属于编码错误,这是程序的bug,解决的方式应该是修改代码,避免发生。例如<code>read property of "undefined"</code>、调用一个异步函数但没有传入callback、函数参数预期是<code>Object</code>但是传了一个<code>String</code>等等。
人们在谈论错误时,总是将这两种错误混在一起,实际上这两种错误是完全不同的。例如<code>File not found</code>是一种操作错误,但这不能说明哪里出错了,这可能仅仅表示程序应该先创建文件。
有些时候,同一个问题可能会导致多种错误。例如nodejs应用因为一个变量undefined导致crash,这是编码错误,客户端则会接收到<code>ECONNRESET</code>错误,这属于操作错误,对于客户端来说应该可以预期到服务器的这个错误。
对于明确的操作错误类型,直接处理掉。
例如尝试打开一个log文件可能会导致 ENOENT ,那么创建这个文件即可。
对于预料之外你不知道如何处理的错误,比较好的方式是记录error并crash,传递合适的错误信息给客户端。
最好的方式是立即crash。
这种错误是程序的bug,一般来说写再多的代码也避免不了。因为在node应用中,我们一般会监控挂掉的进程并自动重启,所以立即crash是比较好的方式。
调试这类问题的最佳方式,是在捕获到<code>uncaught exception</code>的时候,记录相关信息。
总之记住,server的代码错误(bug)传递到client时会成为一个操作错误,例如server捕获到<code>uncaught exception</code>则返回一个500,客户端来处理这个操作错误。
首先,最重要的是文档,描述这个函数做了些什么,接收什么类型的参数返回什么,可能会触发什么错误。
一些基本原则:
同步的函数里,使用<code>throw</code>。使用者使用<code>try...catch</code>即可捕获错误。
异步函数里,更常用的方式是使用<code>callback(err, result)</code>的方式。
在更复杂的场景里,可以返回一个<code>EventEmitter</code>对象,代替使用<code>callback</code>。使用者可以监听<code>emitter</code>对象的 <code>error</code>事件。 例如读取一个数据流,我们可能会同时使用 <code>req.on('data')</code>、<code>req.on('error')</code>、<code>req.on('timeout')</code>。
所以,使用<code>throw</code>还是<code>callbacks</code>、<code>EventEmitter</code>,取决于:
该错误是操作错误还是编码错误?
该函数是同步还是异步?
此外,不管是同步(使用throw)或者异步(使用<code>callback</code>或<code>EventEmitter</code>),只使用一种方式传递错误,避免同时使用两种方式。这样的话,使用者就只需要使用一种方式来捕获错误,例如<code>try...catch</code>或者<code>callback</code>,不需要考虑更多的场景。
下面用一个特例来说明这一点:
在上例的第二种情况,会立即返回<code>TypeError: path must be a string or Buffer</code>,也就是说内部使用了<code>throw</code>,这种情况是不是和上面提到的有矛盾?
其实并不是,第二种情况属于编码错误(<code>fs.stat</code>只接收路径作为参数但我们给了他一个<code>null</code>),并不是操作错误。编码错误永远不应该被处理。
所以在使用<code>fs.stat</code>的时,使用者仍然只需要处理<code>callback</code>传递的错误,不需要使用<code>try...catch</code>。
这一点取决于函数申明的可以允许的类型,以及你如何来解释它们:
如果得到的参数和申明的类型(不一定是指数据类型,也可能是IP地址、QQ号等类型)不一致,那么属于编码错误(使用者应该使用符合要求的参数)
如果得到的参数和申明的类型一致,但函数不能处理这种情况,那属于操作错误。
你必须决定限制类型的严格程度。
例如需要连接到一个服务器,函数接收一个ip地址作为参数,那么有几种做法:
函数只接收ip地址格式的参数,如果不符合格式,则立即抛出异常。
函数接收任意字符串参数,如果参数不是ip地址格式,则使用callback发出一个异步错误,提示无法连接该地址。
这两种做法决定了同样的输入会导致编码错误或操作错误。对于大多数功能,我们强烈建议更严格,因为更宽松的限制会更容易导致使用错误以及浪费时间。
操作错误一般都可以使用明确的机制来处理(根据具体的错误对应处理,使用<code>try...catch</code>、<code>callback</code>、<code>EventEmitter</code>等)。
domain和全局的异常捕获主要是为了发现和处理未预料到的编码错误。
清楚function的功能
必须明确几点:期待的参数、参数类型、额外约束(IP地址、QQ号码等)。
如果任意一点不匹配,则立即抛出<code>throw</code>异常。
此外,还应该有:
使用方可以预料到的操作错误、如何捕获这些错误、返回值。
所有的erorr都使用Error对象(或者基于Error类的扩展)
所有的error都应该提供<code>name</code>和<code>message</code>属性,并且<code>stack</code>也应该准确可用。
使用<code>name</code>属性来区分错误类型
例如<code>RangeError</code>、<code>TypeError</code>。
不要为每种错误取个名字,例如定义<code>InvalidHostnameError</code>、<code>InvalidIpAddressError</code>这种来表示具体的错误,对于这种错误可以统一用<code>InvalidArgumentError</code>表示错误类型,然后在详细描述里补充更多信息。
增加解释错误细节的属性
例如无法连接到服务器,可以增加一个<code>remoteIp</code> 属性表示试图连接的ip。
如果传递一个较低级别的错误,考虑重新包装错误。
如果函数调用顺序如下:funcA -> funcB -> funcC,funcC返回一个加载配置失败的错误,funcB连接服务器失败。
那么,在funcA中,更希望得到包含这2个错误的信息。所以在funcB中捕获到funcC的错误时,包装并传递这些错误是有价值的。
包装底层的错误信息时,尽可能保留原始的信息,除了名称<code>name</code>,但不要改写原始的error对象。
一个组合多个错误的示例:
这里有一个库可以帮我们做这件事:
区分错误类型,是可预见的还是不可避免的,是操作错误还是bug。
操作错误应该被处理。编码错误不应该被处理(全局处理并记录)。
一个函数可能产生的操作错误,只应该使用同步(<code>throw</code>)或者异步一种方式。一般来说,在nodejs中,同步函数导致的操作错误是比较少见的,使用<code>try...catch</code>会很少,常见的是用户输入验证如JSON、解析等。
一个函数的参数、类型、预期错误、如何捕获都应该是明确的。
缺少参数、参数无效都属于编码错误,应该直接抛出异常(<code>throw</code>)。
使用标准的Error类和标准属性。使用独立的属性,添加尽可能多的附加信息,尽可能使用通用的属性名称。
例如一些常见的属性名称:
<code>localHostname、localIp、localPort、remoteHostname、remoteIp、remotePort、path、srcpath、dstpath、hostname、ip、propertyName、propertyValue、syscall、errno</code>
不要尝试用<code>try...catch</code>去捕获一个异步函数的错误,这样会什么也得不到。
如果不是产生错误,不要使用<code>throw</code>。
nodejs之前就已经有操作错误、编码错误的概念,参考这里 https://en.wikipedia.org/wiki/Assertion_%28software_development%29#Comparison_with_error_handling
原文链接:http://ivweb.io/topic/5846d1d4270eedfd10a0f5eb