我们已经谈论了很多指导原则,现在让我们具体一些。
1. 你的函数做什么得很清楚。
这点非常重要。每个接口函数的文档都要很清晰的说明: - 预期参数 - 参数的类型 - 参数的额外约束(例如,必须是有效的IP地址)
如果其中有一点不正确或者缺少,那就是一个程序员的失误,你应该立刻抛出来。
此外,你还要记录:
* 调用者可能会遇到的操作失败(以及它们的`name`)
* 怎么处理操作失败(例如是抛出,传给回调函数,还是被 EventEmitter 发出)
* 返回值
1. 使用 Error 对象或它的子类,并且实现 Error 的协议。
你的所有错误要么使用 Error 类要么使用它的子类。你应该提供`name`和`message`属性,`stack`也是(注意准确)。
1. 在程序里通过 Error 的 `name` 属性区分不同的错误。
当你想要知道错误是何种类型的时候,用name属性。 JavaScript内置的供你重用的名字包括“RangeError”(参数超出有效范围)和“TypeError”(参数类型错误)。而HTTP异常,通常会用RFC指定的名字,比如“BadRequestError”或者“ServiceUnavailableError”。
不要想着给每个东西都取一个新的名字。如果你可以只用一个简单的InvalidArgumentError,就不要分成 InvalidHostnameError,InvalidIpAddressError,InvalidDnsError等等,你要做的是通过增加属性来说明那里出了问题(下面会讲到)。
1. 用详细的属性来增强 Error 对象。
举个例子,如果遇到无效参数,把 `propertyName` 设成参数的名字,把 `propertyValue` 设成传进来的值。如果无法连到服务器,用 `remoteIp` 属性指明尝试连接到的 IP。如果发生一个系统错误,在`syscal` 属性里设置是哪个系统调用,并把错误代码放到`errno`属性里。具体你可以查看附录,看有哪些样例属性可以用。
至少需要这些属性:
`name`:用于在程序里区分众多的错误类型(例如参数非法和连接失败)
`message`:一个供人类阅读的错误消息。对可能读到这条消息的人来说这应该已经足够完整。如果你从更底层的地方传递了一个错误,你应该加上一些信息来说明你在做什么。怎么包装异常请往下看。
`stack`:一般来讲不要随意扰乱堆栈信息。甚至不要增强它。V8引擎只有在这个属性被读取的时候才会真的去运算,以此大幅提高处理异常时候的性能。如果你读完再去增强它,结果就会多付出代价,哪怕调用者并不需要堆栈信息。
你还应该在错误信息里提供足够的消息,这样调用者不用分析你的错误就可以新建自己的错误。它们可能会本地化这个错误信息,也可能想要把大量的错误聚集到一起,再或者用不同的方式显示错误信息(比如在网页上的一个表格里,或者高亮显示用户错误输入的字段)。
1. 若果你传递一个底层的错误给调用者,考虑先包装一下。
经常会发现一个异步函数`funcA`调用另外一个异步函数`funcB`,如果`funcB`抛出了一个错误,希望`funcA`也抛出一模一样的错误。(请注意,第二部分并不总是跟在第一部分之后。有的时候`funcA`会重新尝试。有的时候又希望`funcA`忽略错误因为无事可做。但在这里,我们只讨论`funcA`直接返回`funcB`错误的情况)
在这个例子里,可以考虑包装这个错误而不是直接返回它。包装的意思是继续抛出一个包含底层信息的新的异常,并且带上当前层的上下文。用 **`verror`** 这个包可以很简单的做到这点。
举个例子,假设有一个函数叫做 `fetchConfig`,这个函数会到一个远程的数据库取得服务器的配置。你可能会在服务器启动的时候调用这个函数。整个流程看起来是这样的:
1.加载配置 1.1 连接数据库 1.1.1 解析数据库服务器的DNS主机名 1.1.2 建立一个到数据库服务器的TCP连接 1.1.3 向数据库服务器认证 1.2 发送DB请求 1.3 解析返回结果 1.4 加载配置 2 开始处理请求
假设在运行时出了一个问题连接不到数据库服务器。如果连接在 1.1.2 的时候因为没有到主机的路由而失败了,每个层都不加处理地都把异常向上抛出给调用者。你可能会看到这样的异常信息:
~~~
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`,因为即使是读取它也是相对昂贵的。如果调用者想要一个合并后的堆栈,它应该遍历错误原因并打印每一个错误的堆栈。
在Joyent,我们使用 **`verror`** 这个模块来封装错误,因为它的语法简洁。写这篇文章的时候,它还不能支持上面的所有功能,但是会被扩���以期支持。