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