2015年(13)
分类: Python/Ruby
2015-05-09 10:01:56
- 我写的函数里什么时候该抛出异常,什么时候该传给callback,什么时候触发EventEmitter等等。
- 我的函数对参数该做出怎样的假设?我应该检查更加具体的约束么?例如参数是否非空,是否大于零,是不是看起来像个IP地址,等等等。
- 我该如何处理那些不符合预期的参数?我是应该抛出一个异常,还是把错误传递给一个callback。
- 我该怎么在程序里区分不同的异常(比如“请求错误”和“服务不可用”)?
- 我怎么才能提供足够的信息让调用者知晓错误细节。
- 我该怎么处理未预料的出错?我是应该用 try/catch ,domains 还是其它什么方式呢?
- 背景:希望你所具备的知识。
- 操作失败和程序员的失误:介绍两种基本的异常。
- 编写新函数的实践:关于怎么让函数产生有用报错的基本原则。
- 编写新函数的具体推荐:编写能产生有用报错的、健壮的函数需要的一个检查列表
- 例子:以connect函数为例的文档和序言。
- 总结:全文至此的观点总结。
- 附录:Error对象属性约定:用标准方式提供一个属性列表,以提供更多信息。
throw new Error('something bad happened');
callback(new Error('something bad happened'));
- 操作失败是正确编写的程序在运行时产生的错误。它并不是程序的Bug,反而经常是其它问题:系统本身(内存不足或者打开文件数过多),系统配置(没有到达远程主机的路由),网络问题(端口挂起),远程服务(500错误,连接失败)。例子如下:
- 连接不到服务器
- 无法解析主机名
- 无效的用户输入
- 请求超时
- 服务器返回500
- 套接字被挂起
系统内存不足
程序员失误是程序里的Bug。这些错误往往可以通过修改代码避免。它们永远都没法被有效的处理。- 读取 undefined 的一个属性
- 调用异步函数没有指定回调
- 该传对象的时候传了一个字符串
- 该传IP地址的时候传了一个对象
直接处理。有的时候该做什么很清楚。如果你在尝试打开日志文件的时候得到了一个ENOENT错误,很有可能你是第一次打开这个文件,你要做的就是首先创建它。更有意思的例子是,你维护着到服务器(比如数据库)的持久连接,然后遇到了一个“socket hang-up”的异常。这通常意味着要么远端要么本地的网络失败了。很多时候这种错误是暂时的,所以大部分情况下你得重新连接来解决问题。(这和接下来的重试不大一样,因为在你得到这个错误的时候不一定有操作正在进行)
把出错扩散到客户端。如果你不知道怎么处理这个异常,最简单的方式就是放弃你正在执行的操作,清理所有开始的,然后把错误传递给客户端。(怎么传递异常是另外一回事了,接下来会讨论)。这种方式适合错误短时间内无法解决的情形。比如,用户提交了不正确的JSON,你再解析一次是没什么帮助的。
重试操作。对于那些来自网络和远程服务的错误,有的时候重试操作就可以解决问题。比如,远程服务返回了503(服务不可用错误),你可能会在几秒种后重试。如果确定要重试,你应该清晰的用文档记录下将会多次重试,重试多少次直到失败,以及两次重试的间隔。 另外,不要每次都假设需要重试。如果在栈中很深的地方(比如,被一个客户端调用,而那个客户端被另外一个由用户操作的客户端控制),这种情形下快速失败让客户端去重试会更好。如果栈中的每一层都觉得需要重试,用户最终会等待更长的时间,因为每一层都没有意识到下层同时也在尝试。
直接崩溃。对于那些本不可能发生的错误,或者由程序员失误导致的错误(比如无法连接到同一程序里的本地套接字),可以记录一个错误日志然后直接崩溃。其它的比如内存不足这种错误,是JavaScript这样的脚本语言无法处理的,崩溃是十分合理的。(即便如此,在child_process.exec这样的分离的操作里,得到ENOMEM错误,或者那些你可以合理处理的错误时,你应该考虑这么做)。在你无计可施需要让管理员做修复的时候,你也可以直接崩溃。如果你用光了所有的文件描述符或者没有访问配置文件的权限,这种情况下你什么都做不了,只能等某个用户登录系统把东西修好。
记录错误,其他什么都不做。有的时候你什么都做不了,没有操作可以重试或者放弃,没有任何理由崩溃掉应用程序。举个例子吧,你用DNS跟踪了一组远程服务,结果有一个DNS失败了。除了记录一条日志并且继续使用剩下的服务以外,你什么都做不了。但是,你至少得记录点什么(凡事都有例外。如果这种情况每秒发生几千次,而你又没法处理,那每次发生都记录可能就不值得了,但是要周期性的记录)。
- 从定义上看,这些错误属于Bug。我们并不是在讨论正常的系统或是网络错误,而是程序里实际存在的Bug。它们应该在线上很罕见,并且是调试和修复的最高优先级。
- 上面讨论的种种情形里,请求没有必要一定得成功完成。请求可能成功完成,可能让服务器再次崩溃,可能以某种明显的方式不正确的完成,或者以一种很难调试的方式错误的结束了。
- 在一个完备的分布式系统里,客户端必须能够通过重连和重试来处理服务端的错误。不管 NodeJS 应用程序是否被允许崩溃,网络和系统的失败已经是一个事实了。
- 如果你的线上代码如此频繁地崩溃让连接断开变成了问题,那么正真的问题是你的服务器Bug太多了,而不是因为你选择出错就崩溃。
throw以同步的方式传递异常--也就是在函数被调用处的相同的上下文。如果调用者(或者调用者的调用者)用了try/catch,则异常可以捕获。如果所有的调用者都没有用,那么程序通常情况下会崩溃(异常也可能会被domains或者进程级的uncaughtException捕捉到,详见下文)。
Callback是最基础的异步传递事件的一种方式。用户传进来一个函数(callback),之后当某个异步操作完成后调用这个 callback。通常callback 会以callback(err,result)的形式被调用,这种情况下, err和result必然有一个是非空的,取决于操作是成功还是失败。
更复杂的情形是,函数没有用 Callback 而是返回一个 EventEmitter 对象,调用者需要监听这个对象的 error事件。这种方式在两种情况下很有用。
当你在做一个可能会产生多个错误或多个结果的复杂操作的时候。比如,有一个请求一边从数据库取数据一边把数据发送回客户端,而不是等待所有的结果一起到达。在这个例子里,没有用 callback,而是返回了一个EventEmitter,每个结果会触发一个row 事件,当所有结果发送完毕后会触发end事件,出现错误时会触发一个error事件。
- 这是操作失败还是程序员的失误?
- 这个函数本身是同步的还是异步的。
函数 | 类型 | 错误 | 错误类型 | 传递方式 | 调用者 |
fs.stat | 异步 | file not found | 操作失败 | callback | handle |
JSON.parse | 同步 | bad user input | 操作失败 | throw | try/catch |
fs.stat | 异步 | null for filename | 失误 | throw | none (crash) |
- 在文档里写清楚只接受有效的IPV4的地址,当用户传进来“bob”的时候抛出一个异常。强烈推荐这种做法。
- 在文档里写上接受任何string类型的参数。如果用户传的是“bob”,触发一个异步错误指明无法连接到“bob”这个IP地址。
- 调用者可能会遇到的操作失败(以及它们的name)
- 怎么处理操作失败(例如是抛出,传给回调函数,还是被 EventEmitter 发出)
- 返回值
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.
保持原有的异常完整不变,保证当调用者想要直接用的时候底层的异常还可用。
要么用原有的名字,要么显示地选择一个更有意义的名字。例如,最底层是 NodeJS 报的一个简单的Error,但在步骤1中可以是个 IntializationError 。(但是如果程序可以通过其它的属性区分,不要觉得有责任取一个新的名字)
保留原错误的所有属性。在合适的情况下增强message属性(但是不要在原始的异常上修改)。浅拷贝其它的像是syscall,errno这类的属性。最好是直接拷贝除了 name,message和stack以外的所有属性,而不是硬编码等待拷贝的属性列表。不要理会stack,因为即使是读取它也是相对昂贵的。如果调用者想要一个合并后的堆栈,它应该遍历错误原因并打印每一个错误的堆栈。
参数,类型以及其它一些约束被清晰的文档化。
这个函数对于接受的参数是非常严格的,并且会在得到错误参数的时候抛出异常(程序员的失误)。
可能出现的操作失败集合被记录了。通过不同的”name“值可以区分不同的异常,而”errno“被用来获得系统错误的详细信息。
异常被传递的方式也被记录了(通过失败时调用回调函数)。
返回的错误有”remoteIp“和”remotePort“字段,这样用户就可以定义自己的错误了(比如,一个HTTP客户端的端口号是隐含的)。
虽然很明显,但是连接失败后的状态也被清晰的记录了:所有被打开的套接字此时已经被关闭。
学习了怎么区分操作失败,即那些可以被预测的哪怕在正确的程序里也无法避免的错误(例如,无法连接到服务器);而程序的Bug则是程序员失误。
操作失败可以被处理,也应当被处理。程序员的失误无法被处理或可靠地恢复(本不应该这么做),尝试这么做只会让问题更难调试。
一个给定的函数,它处理异常的方式要么是同步(用throw方式)要么是异步的(用callback或者EventEmitter),不会两者兼具。用户可以在回调函数里处理错误,也可以使用 try/catch捕获异常 ,但是不能一起用。实际上,使用throw并且期望调用者使用 try/catch 是很罕见的,因为 NodeJS里的同步函数通常不会产生运行失败(主要的例外是类似于JSON.parse的用户输入验证函数)。
在写新函数的时候,用文档清楚地记录函数预期的参数,包括它们的类型、是否有其它约束(例如必须是有效的IP地址),可能会发生的合理的操作失败(例如无法解析主机名,连接服务器失败,所有的服务器端错误),错误是怎么传递给调用者的(同步,用throw,还是异步,用 callback 和EventEmitter)。
缺少参数或者参数无效是程序员的失误,一旦发生总是应该抛出异常。函数的作者认为的可接受的参数可能会有一个灰色地带,但是如果传递的是一个文档里写明接收的参数以外的东西,那就是一个程序员失误。
传递错误的时候用标准的 Error 类和它标准的属性。尽可能把额外的有用信息放在对应的属性里。如果有可能,用约定的属性名(如下)。
Property name | Intended use |
localHostname | the local DNS hostname (e.g., that you're accepting connections at) |
localIp | the local IP address (e.g., that you're accepting connections at) |
localPort | the local TCP port (e.g., that you're accepting connections at) |
remoteHostname | the DNS hostname of some other service (e.g., that you tried to connect to) |
remoteIp | the IP address of some other service (e.g., that you tried to connect to) |
remotePort | the port of some other service (e.g., that you tried to connect to) |
path | the name of a file, directory, or Unix Domain Socket (e.g., that you tried to open) |
srcpath | the name of a path used as a source (e.g., for a rename or copy) |
dstpath | the name of a path used as a destination (e.g., for a rename or copy) |
hostname | a DNS hostname (e.g., that you tried to resolve) |
ip | an IP address (e.g., that you tried to reverse-resolve) |
propertyName | an object property name, or an argument name (e.g., for a validation error) |
propertyValue | an object property value (e.g., for a validation error) |
syscall | the name of a system call that failed |
errno | the symbolic value of errno (e.g., "ENOENT"). Do not use this for errors that don't actually set the C value of errno.Use "name" to distinguish between types of errors. |