我們已經(jīng)討論了如何處理異常,那么當(dāng)你在編寫新的函數(shù)的時候,怎么才能向調(diào)用者傳遞錯誤呢?
最最重要的一點是為你的函數(shù)寫好文檔,包括它接受的參數(shù)(附上類型和其它約束),返回值,可能發(fā)生的錯誤,以及這些錯誤意味著什么。?**如果你不知道會導(dǎo)致什么錯誤或者不了解錯誤的含義,那你的應(yīng)用程序正常工作就是一個巧合。**?所以,當(dāng)你編寫新的函數(shù)的時候,一定要告訴調(diào)用者可能發(fā)生哪些錯誤和錯誤的含義。
### Throw, Callback 還是 EventEmitter
函數(shù)有三種基本的傳遞錯誤的模式。
* `throw`以同步的方式傳遞異常--也就是在函數(shù)被調(diào)用處的相同的上下文。如果調(diào)用者(或者調(diào)用者的調(diào)用者)用了`try/catch`,則異??梢圆东@。如果所有的調(diào)用者都沒有用,那么程序通常情況下會崩潰(異常也可能會被`domains`或者進程級的`uncaughtException`捕捉到,詳見下文)。
* Callback 是最基礎(chǔ)的異步傳遞事件的一種方式。用戶傳進來一個函數(shù)(callback),之后當(dāng)某個異步操作完成后調(diào)用這個 callback。通常 callback 會以`callback(err,result)`的形式被調(diào)用,這種情況下, err和 result必然有一個是非空的,取決于操作是成功還是失敗。
* 更復(fù)雜的情形是,函數(shù)沒有用 Callback 而是返回一個 EventEmitter 對象,調(diào)用者需要監(jiān)聽這個對象的 error事件。這種方式在兩種情況下很有用。
* 當(dāng)你在做一個可能會產(chǎn)生多個錯誤或多個結(jié)果的復(fù)雜操作的時候。比如,有一個請求一邊從數(shù)據(jù)庫取數(shù)據(jù)一邊把數(shù)據(jù)發(fā)送回客戶端,而不是等待所有的結(jié)果一起到達。在這個例子里,沒有用 callback,而是返回了一個 EventEmitter,每個結(jié)果會觸發(fā)一個`row`?事件,當(dāng)所有結(jié)果發(fā)送完畢后會觸發(fā)`end`事件,出現(xiàn)錯誤時會觸發(fā)一個`error`事件。
* 用在那些具有復(fù)雜狀態(tài)機的對象上,這些對象往往伴隨著大量的異步事件。例如,一個套接字是一個EventEmitter,它可能會觸發(fā)“connect“,”end“,”timeout“,”drain“,”close“事件。這樣,很自然地可以把”error“作為另外一種可以被觸發(fā)的事件。在這種情況下,清楚知道”error“還有其它事件何時被觸發(fā)很重要,同時被觸發(fā)的還有什么事件(例如”close“),觸發(fā)的順序,還有套接字是否在結(jié)束的時候處于關(guān)閉狀態(tài)。
在大多數(shù)情況下,我們會把 callback 和 event emitter 歸到同一個“異步錯誤傳遞”籃子里。如果你有傳遞異步錯誤的需要,你通常只要用其中的一種而不是同時使用。
那么,什么時候用`throw`,什么時候用callback,什么時候又用 EventEmitter 呢?這取決于兩件事:
* 這是操作失敗還是程序員的失誤?
* 這個函數(shù)本身是同步的還是異步的。
直到目前,最常見的例子是在異步函數(shù)里發(fā)生了操作失敗。在大多數(shù)情況下,你需要寫一個以回調(diào)函數(shù)作為參數(shù)的函數(shù),然后你會把異常傳遞給這個回調(diào)函數(shù)。這種方式工作的很好,并且被廣泛使用。例子可參照 NodeJS 的`fs`模塊。如果你的場景比上面這個還復(fù)雜,那么你可能就得換用 EventEmitter 了,不過你也還是在用異步方式傳遞這個錯誤。
其次常見的一個例子是像`JSON.parse`這樣的函數(shù)同步產(chǎn)生了一個異常。對這些函數(shù)而言,如果遇到操作失敗(比如無效輸入),你得用同步的方式傳遞它。你可以拋出(更加常見)或者返回它。
對于給定的函數(shù),如果有一個異步傳遞的異常,那么所有的異常都應(yīng)該被異步傳遞??赡苡羞@樣的情況,請求一到來你就知道它會失敗,并且知道不是因為程序員的失誤??赡艿那樾问悄憔彺媪朔祷亟o最近請求的錯誤。雖然你知道請求一定失敗,但是你還是應(yīng)該用異步的方式傳遞它。
通用的準則就是?**你即可以同步傳遞錯誤(拋出),也可以異步傳遞錯誤(通過傳給一個回調(diào)函數(shù)或者觸發(fā)EventEmitter的?`error`事件),但是不用同時使用**。以這種方式,用戶處理異常的時候可以選擇用回調(diào)函數(shù)還是用`try/catch`,但是不需要兩種都用。具體用哪一個取決于異常是怎么傳遞的,這點得在文檔里說明清楚。
差點忘了程序員的失誤?;貞浺幌?,它們其實是Bug。在函數(shù)開頭通過檢查參數(shù)的類型(或是其它約束)就可以被立即發(fā)現(xiàn)。一個退化的例子是,某人調(diào)用了一個異步的函數(shù),但是沒有傳回調(diào)函數(shù)。你應(yīng)該立刻把這個錯拋出,因為程序已經(jīng)出錯而在這個點上最好的調(diào)試的機會就是得到一個堆棧信息,如果有內(nèi)核信息就更好了。
因為程序員的失誤永遠不應(yīng)該被處理,上面提到的調(diào)用者只能用`try/catch`或者回調(diào)函數(shù)(或者 EventEmitter)其中一種處理異常的準則并沒有因為這條意見而改變。如果你想知道更多,請見上面的 (不要)處理程序員的失誤。
下表以 NodeJS 核心模塊的常見函數(shù)為例,做了一個總結(jié),大致按照每種問題出現(xiàn)的頻率來排列:
| 函數(shù) | 類型 | 錯誤 | 錯誤類型 | 傳遞方式 | 調(diào)用者 |
| --- | --- | --- | --- | --- | --- |
| `fs.stat` | 異步 | file not found | 操作失敗 | callback | handle |
| `JSON.parse` | 同步 | bad user input | 操作失敗 | throw | `try/catch` |
| `fs.stat` | 異步 | null for filename | 失誤 | throw | none (crash) |
異步函數(shù)里出現(xiàn)操作錯誤的例子(第一行)是最常見的。在同步函數(shù)里發(fā)生操作失敗(第二行)比較少見,除非是驗證用戶輸入。程序員失誤(第三行)除非是在開發(fā)環(huán)境下,否則永遠都不應(yīng)該出現(xiàn)。
_吐槽:程序員失誤還是操作失敗?_
你怎么知道是程序員的失誤還是操作失敗呢?很簡單,你自己來定義并且記在文檔里,包括允許什么類型的函數(shù),怎樣打斷它的執(zhí)行。如果你得到的異常不是文檔里能接受的,那就是一個程序員失誤。如果在文檔里寫明接受但是暫時處理不了的,那就是一個操作失敗。
你得用你的判斷力去決定你想做到多嚴格,但是我們會給你一定的意見。具體一些,想象有個函數(shù)叫做“connect”,它接受一個IP地址和一個回調(diào)函數(shù)作為參數(shù),這個回調(diào)函數(shù)會在成功或者失敗的時候被調(diào)用?,F(xiàn)在假設(shè)用戶傳進來一個明顯不是IP地址的參數(shù),比如`“bob”`,這個時候你有幾種選擇:
* 在文檔里寫清楚只接受有效的IPV4的地址,當(dāng)用戶傳進來`“bob”`的時候拋出一個異常。強烈推薦這種做法。
* 在文檔里寫上接受任何string類型的參數(shù)。如果用戶傳的是`“bob”`,觸發(fā)一個異步錯誤指明無法連接到`“bob”`這個IP地址。
這兩種方式和我們上面提到的關(guān)于操作失敗和程序員失誤的指導(dǎo)原則是一致的。你決定了這樣的輸入算是程序員的失誤還是操作失敗。通常,用戶輸入的校驗是很松的,為了證明這點,可以看`Date.parse`這個例子,它接受很多類型的輸入。但是對于大多數(shù)其它函數(shù),我們強烈建議你偏向更嚴格而不是更松。你的程序越是猜測用戶的本意(使用隱式的轉(zhuǎn)換,無論是JavaScript語言本身這么做還是有意為之),就越是容易猜錯。本意是想讓開發(fā)者在使用的時候不用更加具體,結(jié)果卻耗費了人家好幾個小時在Debug上。再說了,如果你覺得這是個好主意,你也可以在未來的版本里讓函數(shù)不那么嚴格,但是如果你發(fā)現(xiàn)由于猜測用戶的意圖導(dǎo)致了很多惱人的bug,要修復(fù)它的時候想保持兼容性就不大可能了。
所以如果一個值怎么都不可能是有效的(本該是string卻得到一個`undefined`,本該是string類型的IP但明顯不是),你應(yīng)該在文檔里寫明是這不允許的并且立刻拋出一個異常。只要你在文檔里寫的清清楚楚,那這就是一個程序員的失誤而不是操作失敗。立即拋出可以把Bug帶來的損失降到最小,并且保存了開發(fā)者可以用來調(diào)試這個問題的信息(例如,調(diào)用堆棧,如果用內(nèi)核文件還可以得到參數(shù)和內(nèi)存分布)。
那么?`domains`?和?`process.on('uncaughtException')`?呢?
操作失敗總是可以被顯示的機制所處理的:捕獲一個異常,在回調(diào)里處理錯誤,或者處理EventEmitter的“error”事件等等。`Domains`以及進程級別的`‘uncaughtException’`主要是用來從未料到的程序錯誤恢復(fù)的。由于上面我們所討論的原因,這兩種方式都不鼓勵。
