我們已經談論了很多指導原則,現在讓我們具體一些。
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`**?這個模塊來封裝錯誤,因為它的語法簡潔。寫這篇文章的時候,它還不能支持上面的所有功能,但是會被擴???以期支持。