nodejs 中错误捕获的一些最佳实践

作者:vienwu

本文内容大部分来自 https://www.joyent.com/node-js/production/design/errors ,原文比较长,感觉也有点啰嗦,所以根据个人理解猜测梳理出本文,如果有错误欢迎指出,谢谢!

很多人其实不是很重视错误处理,但对于构建一个健壮的nodejs应用,错误处理是非常重要的一件事情,希望本文可以给你一些启发。

先抛出几个问题:

  1. 应该用哪种方式暴露错误?throwcallback(err, result)Event Emitter或者其他方式?
  2. 如何假设函数的参数?是否应该检测类型正确?非null,IP,QQ号码?
  3. 函数参数不符合预期该怎么处理?
  4. 应该如何区分不同类型的错误?例如Bad RequestService Unavailable
  5. 应该如何提供有用的错误信息?
  6. 应该如何捕获错误?使用try/catch,还是domains或者其他方式?

一些基础知识

关于Errorthrowtry...catch的一些基础知识链接

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 domainprocess https://nodejs.org/api/domain.html https://nodejs.org/api/process.html

verror模块: rich JavaScript errors https://github.com/joyent/node-verror

抛出错误的几种方式:

var myEmitter = new MyEmitter();
doSomeAsynchronousOperation(function (err) {
      if (err) throw (err); // 直接throw
      if (err) callback(err); // 使用callback,nodejs中常见的异步处理方式
      myEmitter.emit('error', new Error('whoops!')); // error事件
});

捕获错误

try{
    var result = JSON.parse(str);
}catch(e){
    // 捕获错误
}

分类错误

一般来说,我们将错误简单的分为两种类型:操作错误、编码错误。

对于有经验的人来说,写代码的时候都会处理一些常见的操作错误,例如JSON.parse总是会和try...catch一起,例如网络故障、远程服务器返回500等。这些错误并非bug。

对于程序来说,另外一种错误属于编码错误,这是程序的bug,解决的方式应该是修改代码,避免发生。例如read property of "undefined"、调用一个异步函数但没有传入callback、函数参数预期是Object但是传了一个String等等。

人们在谈论错误时,总是将这两种错误混在一起,实际上这两种错误是完全不同的。例如File not found是一种操作错误,但这不能说明哪里出错了,这可能仅仅表示程序应该先创建文件。

有些时候,同一个问题可能会导致多种错误。例如nodejs应用因为一个变量undefined导致crash,这是编码错误,客户端则会接收到ECONNRESET错误,这属于操作错误,对于客户端来说应该可以预期到服务器的这个错误。

如何处理 操作错误

  • 对于明确的操作错误类型,直接处理掉。 例如尝试打开一个log文件可能会导致 ENOENT ,那么创建这个文件即可。
  • 对于预料之外你不知道如何处理的错误,比较好的方式是记录error并crash,传递合适的错误信息给客户端。

如何处理 代码错误

最好的方式是立即crash。

这种错误是程序的bug,一般来说写再多的代码也避免不了。因为在node应用中,我们一般会监控挂掉的进程并自动重启,所以立即crash是比较好的方式。

调试这类问题的最佳方式,是在捕获到uncaught exception的时候,记录相关信息。

总之记住,server的代码错误(bug)传递到client时会成为一个操作错误,例如server捕获到uncaught exception则返回一个500,客户端来处理这个操作错误。

如何传递错误?

首先,最重要的是文档,描述这个函数做了些什么,接收什么类型的参数返回什么,可能会触发什么错误。

一些基本原则:

  • 同步的函数里,使用throw。使用者使用try...catch即可捕获错误。
  • 异步函数里,更常用的方式是使用callback(err, result)的方式。
  • 在更复杂的场景里,可以返回一个EventEmitter对象,代替使用callback。使用者可以监听emitter对象的 error事件。 例如读取一个数据流,我们可能会同时使用 req.on('data')req.on('error')req.on('timeout')

所以,使用throw还是callbacksEventEmitter,取决于:

  • 该错误是操作错误还是编码错误?
  • 该函数是同步还是异步?

此外,不管是同步(使用throw)或者异步(使用callbackEventEmitter),只使用一种方式传递错误,避免同时使用两种方式。这样的话,使用者就只需要使用一种方式来捕获错误,例如try...catch或者callback,不需要考虑更多的场景。

下面用一个特例来说明这一点:

// 异步函数,err是操作错误,使用callback传递
fs.stat('不存在的文件',function(err){}) 
// 异步函数,参数错误,会立即抛出异常
fs.stat(null,function(err){})

在上例的第二种情况,会立即返回TypeError: path must be a string or Buffer,也就是说内部使用了throw,这种情况是不是和上面提到的有矛盾?

其实并不是,第二种情况属于编码错误(fs.stat只接收路径作为参数但我们给了他一个null),并不是操作错误。编码错误永远不应该被处理。

所以在使用fs.stat的时,使用者仍然只需要处理callback传递的错误,不需要使用try...catch

错误的输入属于哪种情况?编码错误还是操作错误?

这一点取决于函数申明的可以允许的类型,以及你如何来解释它们:

  • 如果得到的参数和申明的类型(不一定是指数据类型,也可能是IP地址、QQ号等类型)不一致,那么属于编码错误(使用者应该使用符合要求的参数)
  • 如果得到的参数和申明的类型一致,但函数不能处理这种情况,那属于操作错误。

你必须决定限制类型的严格程度。

例如需要连接到一个服务器,函数接收一个ip地址作为参数,那么有几种做法:

  • 函数只接收ip地址格式的参数,如果不符合格式,则立即抛出异常。
  • 函数接收任意字符串参数,如果参数不是ip地址格式,则使用callback发出一个异步错误,提示无法连接该地址。

这两种做法决定了同样的输入会导致编码错误或操作错误。对于大多数功能,我们强烈建议更严格,因为更宽松的限制会更容易导致使用错误以及浪费时间。

什么时候使用domainprocess.on('uncaughtException') ?

操作错误一般都可以使用明确的机制来处理(根据具体的错误对应处理,使用try...catchcallbackEventEmitter等)。

domain和全局的异常捕获主要是为了发现和处理未预料到的编码错误。

编写functions的具体建议

  • 清楚function的功能

必须明确几点:期待的参数、参数类型、额外约束(IP地址、QQ号码等)。

如果任意一点不匹配,则立即抛出throw异常。

此外,还应该有:

使用方可以预料到的操作错误、如何捕获这些错误、返回值。

  • 所有的erorr都使用Error对象(或者基于Error类的扩展) 所有的error都应该提供namemessage属性,并且stack也应该准确可用。
  • 使用name属性来区分错误类型

例如RangeErrorTypeError

不要为每种错误取个名字,例如定义InvalidHostnameErrorInvalidIpAddressError这种来表示具体的错误,对于这种错误可以统一用InvalidArgumentError表示错误类型,然后在详细描述里补充更多信息。

  • 增加解释错误细节的属性

例如无法连接到服务器,可以增加一个remoteIp 属性表示试图连接的ip。

  • 如果传递一个较低级别的错误,考虑重新包装错误。

如果函数调用顺序如下:funcA -> funcB -> funcC,funcC返回一个加载配置失败的错误,funcB连接服务器失败。

那么,在funcA中,更希望得到包含这2个错误的信息。所以在funcB中捕获到funcC的错误时,包装并传递这些错误是有价值的。

包装底层的错误信息时,尽可能保留原始的信息,除了名称name,但不要改写原始的error对象。

一个组合多个错误的示例:

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

这里有一个库可以帮我们做这件事:

https://github.com/joyent/node-verror

总结

  • 区分错误类型,是可预见的还是不可避免的,是操作错误还是bug。
  • 操作错误应该被处理。编码错误不应该被处理(全局处理并记录)。
  • 一个函数可能产生的操作错误,只应该使用同步(throw)或者异步一种方式。一般来说,在nodejs中,同步函数导致的操作错误是比较少见的,使用try...catch会很少,常见的是用户输入验证如JSON、解析等。
  • 一个函数的参数、类型、预期错误、如何捕获都应该是明确的。
  • 缺少参数、参数无效都属于编码错误,应该直接抛出异常(throw)。
  • 使用标准的Error类和标准属性。使用独立的属性,添加尽可能多的附加信息,尽可能使用通用的属性名称。

例如一些常见的属性名称:

localHostname、localIp、localPort、remoteHostname、remoteIp、remotePort、path、srcpath、dstpath、hostname、ip、propertyName、propertyValue、syscall、errno

最后

原文链接:http://ivweb.io/topic/5846d1d4270eedfd10a0f5eb

原文链接:

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏orientlu

FreeRTOS 消息队列

上面这几中方式中, 除了消息通知, 其他几种实现都是基于消息队列。消息队列作为主要的通信方式, 支持在任务间, 任务和中断间传递消息内容。 这一章介绍 Fre...

2522
来自专栏后端技术探索

利用 PHP 名称空间编写可读且可维护的代码

“Conan 是我榜样。” 如果我在餐桌上说这句话,我儿子会以为我说的是游戏 “野蛮人柯南”,而我妻子会以为我说的是脱口秀主持人 Conan O'Brien。这...

1132
来自专栏枕边书

PHP模拟发送POST请求之二、用PHP和JS处理URL信息

明白了HTTP请求的头信息后,我们还需要对请求地址有所了解。再者,HTTP GET请求是靠URL实现的,所以了解URL的构造,处理URL的重要性不言而喻。 在P...

1975
来自专栏增长技术

git对象模型

所有用来表示项目历史信息的文件,是通过一个40个字符的(40-digit)“对象名”来索引的,对象名看起来像这样:

1033
来自专栏Ryan Miao

redis学习之二from github

大概敲了一遍基本命令,熟悉了redis的存储方式。现在开始进一步系统的学习。学习教程目前计划有三个,一个是github上的https://github.com/...

2886
来自专栏Linyb极客之路

从Java内存模型角度理解安全初始化

如大家所知,Java代码在编译和运行的过程中会对代码有很多意想不到且不受开发人员控制的操作:

823
来自专栏吴生的专栏

谁说深入浅出虚拟机难?现在我让他通俗易懂(JVM)

1:什么是JVM 大家可以想想,JVM 是什么?JVM是用来干什么的?在这里我列出了三个概念,第一个是JVM,第二个是JDK,第三个是JRE。相信大家对这三个不...

3786
来自专栏Linux驱动

42.Linux应用调试-初步制作系统调用(用户态->内核态)

1首先来讲讲应用程序如何实现系统调用(用户态->内核态)? 我们以应用程序的write()函数为例: 1)首先用户态的write()函数会进入glibc库,里面...

2075
来自专栏linux驱动个人学习

Linux进程ID号--Linux进程的管理与调度(三)【转】

Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,...

1831
来自专栏逍遥剑客的游戏开发

Nebula3学习笔记(5): IO系统

1794

扫码关注云+社区