我們已經(jīng)談?wù)摿撕芏嘀笇?dǎo)原則,現(xiàn)在讓我們具體一些。
這點非常重要。每個接口函數(shù)的文檔都要很清晰的說明: - 預(yù)期參數(shù) - 參數(shù)的類型 - 參數(shù)的額外約束(例如,必須是有效的IP地址)
如果其中有一點不正確或者缺少,那就是一個程序員的失誤,你應(yīng)該立刻拋出來。
此外,你還要記錄:
name
)你的所有錯誤要么使用 Error 類要么使用它的子類。你應(yīng)該提供name
和message
屬性,stack
也是(注意準(zhǔn)確)。
name
?屬性區(qū)分不同的錯誤。當(dāng)你想要知道錯誤是何種類型的時候,用name屬性。 JavaScript內(nèi)置的供你重用的名字包括“RangeError”(參數(shù)超出有效范圍)和“TypeError”(參數(shù)類型錯誤)。而HTTP異常,通常會用RFC指定的名字,比如“BadRequestError”或者“ServiceUnavailableError”。
不要想著給每個東西都取一個新的名字。如果你可以只用一個簡單的InvalidArgumentError,就不要分成 InvalidHostnameError,InvalidIpAddressError,InvalidDnsError等等,你要做的是通過增加屬性來說明那里出了問題(下面會講到)。
舉個例子,如果遇到無效參數(shù),把?propertyName
?設(shè)成參數(shù)的名字,把?propertyValue
?設(shè)成傳進來的值。如果無法連到服務(wù)器,用?remoteIp
?屬性指明嘗試連接到的 IP。如果發(fā)生一個系統(tǒng)錯誤,在syscal
?屬性里設(shè)置是哪個系統(tǒng)調(diào)用,并把錯誤代碼放到errno
屬性里。具體你可以查看附錄,看有哪些樣例屬性可以用。
至少需要這些屬性:
name
:用于在程序里區(qū)分眾多的錯誤類型(例如參數(shù)非法和連接失?。?/p>
message
:一個供人類閱讀的錯誤消息。對可能讀到這條消息的人來說這應(yīng)該已經(jīng)足夠完整。如果你從更底層的地方傳遞了一個錯誤,你應(yīng)該加上一些信息來說明你在做什么。怎么包裝異常請往下看。
stack
:一般來講不要隨意擾亂堆棧信息。甚至不要增強它。V8引擎只有在這個屬性被讀取的時候才會真的去運算,以此大幅提高處理異常時候的性能。如果你讀完再去增強它,結(jié)果就會多付出代價,哪怕調(diào)用者并不需要堆棧信息。
你還應(yīng)該在錯誤信息里提供足夠的消息,這樣調(diào)用者不用分析你的錯誤就可以新建自己的錯誤。它們可能會本地化這個錯誤信息,也可能想要把大量的錯誤聚集到一起,再或者用不同的方式顯示錯誤信息(比如在網(wǎng)頁上的一個表格里,或者高亮顯示用戶錯誤輸入的字段)。
經(jīng)常會發(fā)現(xiàn)一個異步函數(shù)funcA
調(diào)用另外一個異步函數(shù)funcB
,如果funcB
拋出了一個錯誤,希望funcA
也拋出一模一樣的錯誤。(請注意,第二部分并不總是跟在第一部分之后。有的時候funcA
會重新嘗試。有的時候又希望funcA
忽略錯誤因為無事可做。但在這里,我們只討論funcA
直接返回funcB
錯誤的情況)
在這個例子里,可以考慮包裝這個錯誤而不是直接返回它。包裝的意思是繼續(xù)拋出一個包含底層信息的新的異常,并且?guī)袭?dāng)前層的上下文。用?verror
?這個包可以很簡單的做到這點。
舉個例子,假設(shè)有一個函數(shù)叫做?fetchConfig
,這個函數(shù)會到一個遠程的數(shù)據(jù)庫取得服務(wù)器的配置。你可能會在服務(wù)器啟動的時候調(diào)用這個函數(shù)。整個流程看起來是這樣的:
1.加載配置 1.1 連接數(shù)據(jù)庫 1.1.1 解析數(shù)據(jù)庫服務(wù)器的DNS主機名 1.1.2 建立一個到數(shù)據(jù)庫服務(wù)器的TCP連接 1.1.3 向數(shù)據(jù)庫服務(wù)器認(rèn)證 1.2 發(fā)送DB請求 1.3 解析返回結(jié)果 1.4 加載配置 2 開始處理請求
假設(shè)在運行時出了一個問題連接不到數(shù)據(jù)庫服務(wù)器。如果連接在 1.1.2 的時候因為沒有到主機的路由而失敗了,每個層都不加處理地都把異常向上拋出給調(diào)用者。你可能會看到這樣的異常信息:
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。
你可能會想跳過其中幾層的封裝來得到一條不那么充滿學(xué)究氣息的消息:
myserver: failed to load configuration: connection refused from database at 127.0.0.1 port 1234.
不過話又說回來,報錯的時候詳細一點總比信息不夠要好。
如果你決定封裝一個異常了,有幾件事情要考慮:
保持原有的異常完整不變,保證當(dāng)調(diào)用者想要直接用的時候底層的異常還可用。
要么用原有的名字,要么顯示地選擇一個更有意義的名字。例如,最底層是 NodeJS 報的一個簡單的Error,但在步驟1中可以是個 IntializationError 。(但是如果程序可以通過其它的屬性區(qū)分,不要覺得有責(zé)任取一個新的名字)
message
屬性(但是不要在原始的異常上修改)。淺拷貝其它的像是syscall
,errno
這類的屬性。最好是直接拷貝除了?name
,message
和stack
以外的所有屬性,而不是硬編碼等待拷貝的屬性列表。不要理會stack
,因為即使是讀取它也是相對昂貴的。如果調(diào)用者想要一個合并后的堆棧,它應(yīng)該遍歷錯誤原因并打印每一個錯誤的堆棧。在Joyent,我們使用?verror
?這個模塊來封裝錯誤,因為它的語法簡潔。寫這篇文章的時候,它還不能支持上面的所有功能,但是會被擴???以期支持。
更多建議: