NodeJS 的錯(cuò)誤處理讓人痛苦,在很長(zhǎng)的一段時(shí)間里,大量的錯(cuò)誤被放任不管。但是要想建立一個(gè)健壯的 Node.js 程序就必須正確的處理這些錯(cuò)誤,而且這并不難學(xué)。如果你實(shí)在沒(méi)有耐心,那就直接繞過(guò)長(zhǎng)篇大論跳到“總結(jié)”部分吧。
英文原文:https://www.joyent.com/node-js/production/design/errors
這篇文章會(huì)回答 NodeJS 初學(xué)者的若干問(wèn)題:
- 我寫(xiě)的函數(shù)里什么時(shí)候該拋出異常,什么時(shí)候該傳給 callback,什么時(shí)候觸發(fā) EventEmitter 等等。
- 我的函數(shù)對(duì)參數(shù)該做出怎樣的假設(shè)?我應(yīng)該檢查更加具體的約束么?例如參數(shù)是否非空,是否大于零,是不是看起來(lái)像個(gè) IP 地址,等等等。
- 我該如何處理那些不符合預(yù)期的參數(shù)?我是應(yīng)該拋出一個(gè)異常,還是把錯(cuò)誤傳遞給一個(gè) callback。
- 我該怎么在程序里區(qū)分不同的異常(比如“請(qǐng)求錯(cuò)誤”和“服務(wù)不可用”)?
- 我怎么才能提供足夠的信息讓調(diào)用者知曉錯(cuò)誤細(xì)節(jié)。
- 我該怎么處理未預(yù)料的出錯(cuò)?我是應(yīng)該用 try/catch ,domains 還是其它什么方式呢?
這篇文章可以劃分成互相為基礎(chǔ)的幾個(gè)部分:
- 背景:希望你所具備的知識(shí)。
- 操作失敗和程序員的失誤:介紹兩種基本的異常。
- 編寫(xiě)新函數(shù)的實(shí)踐:關(guān)于怎么讓函數(shù)產(chǎn)生有用報(bào)錯(cuò)的基本原則。
- 編寫(xiě)新函數(shù)的具體推薦:編寫(xiě)能產(chǎn)生有用報(bào)錯(cuò)的、健壯的函數(shù)需要的一個(gè)檢查列表。
- 例子:以 connect 函數(shù)為例的文檔和序言。
- 總結(jié):全文至此的觀點(diǎn)總結(jié)。
- 附錄:Error 對(duì)象屬性約定:用標(biāo)準(zhǔn)方式提供一個(gè)屬性列表,以提供更多信息。
背景
本文假設(shè):
你已經(jīng)熟悉了 JavaScript、Java、 Python、 C++ 或者類(lèi)似的語(yǔ)言中異常的概念,而且你知道拋出異常和捕獲異常是什么意思。
你熟悉怎么用 NodeJS 編寫(xiě)代碼。你使用異步操作的時(shí)候會(huì)很自在,并能用 callback(err,result) 模式去完成異步操作。你得知道下面的代碼不能正確處理異常的原因是什么?
function myApiFunc(callback)
{
/*
* This pattern does NOT work!
*/
try {
doSomeAsynchronousOperation(function (err) {
if (err)
throw (err);
/* continue as normal */
});
} catch (ex) {
callback(ex);
}
}
你還要熟悉三種傳遞錯(cuò)誤的方式:
1.作為異常拋出。 2. 把錯(cuò)誤傳給一個(gè) callback,這個(gè)函數(shù)正是為了處理異常和處理異步操作返回結(jié)果的。 3.在 EventEmitter 上觸發(fā)一個(gè) Error 事件。
接下來(lái)我們會(huì)詳細(xì)討論這幾種方式。這篇文章不假設(shè)你知道任何關(guān)于 domains 的知識(shí)。
最后,你應(yīng)該知道在 JavaScript 里,錯(cuò)誤和異常是有區(qū)別的。錯(cuò)誤是 Error 的一個(gè)實(shí)例。錯(cuò)誤被創(chuàng)建并且直接傳遞給另一個(gè)函數(shù)或者被拋出。如果一個(gè)錯(cuò)誤被拋出了那么它就變成了一個(gè)異常。舉個(gè)例子:
throw new Error(‘something bad happened’);
但是使用一個(gè)錯(cuò)誤而不拋出也是可以的:
callback(new Error(‘something bad happened’));
這種用法更常見(jiàn),因?yàn)樵?NodeJS 里,大部分的錯(cuò)誤都是異步的。實(shí)際上,try/catch 唯一常用的是在 JSON.parse 和類(lèi)似驗(yàn)證用戶輸入的地方。接下來(lái)我們會(huì)看到,其實(shí)很少要捕獲一個(gè)異步函數(shù)里的異常。這一點(diǎn)和 Java,C++,以及其它嚴(yán)重依賴(lài)異常的語(yǔ)言很不一樣。
操作失敗和程序員的失誤
把錯(cuò)誤分成兩大類(lèi)很有用:
-
操作失敗是正確編寫(xiě)的程序在運(yùn)行時(shí)產(chǎn)生的錯(cuò)誤。它并不是程序的 Bug,反而經(jīng)常是其它問(wèn)題:系統(tǒng)本身(內(nèi)存不足或者打開(kāi)文件數(shù)過(guò)多),系統(tǒng)配置(沒(méi)有到達(dá)遠(yuǎn)程主機(jī)的路由),網(wǎng)絡(luò)問(wèn)題(端口掛起),遠(yuǎn)程服務(wù)(500錯(cuò)誤,連接失?。@尤缦拢?/p>
- 連接不到服務(wù)器
- 無(wú)法解析主機(jī)名
- 無(wú)效的用戶輸入
- 請(qǐng)求超時(shí)
- 服務(wù)器返回 500
- 套接字被掛起
- 系統(tǒng)內(nèi)存不足
-
程序員失誤是程序里的 Bug。這些錯(cuò)誤往往可以通過(guò)修改代碼避免。它們永遠(yuǎn)都沒(méi)法被有效的處理:
- 讀取 undefined 的一個(gè)屬性
- 調(diào)用異步函數(shù)沒(méi)有指定回調(diào)
- 該傳對(duì)象的時(shí)候傳了一個(gè)字符串
- 該傳IP地址的時(shí)候傳了一個(gè)對(duì)象
人們把操作失敗和程序員的失誤都稱(chēng)為“錯(cuò)誤”,但其實(shí)它們很不一樣。操作失敗是所有正確的程序應(yīng)該處理的錯(cuò)誤情形,只要被妥善處理它們不一定會(huì)預(yù)示著 Bug 或是嚴(yán)重的問(wèn)題。“文件找不到”是一個(gè)操作失敗,但是它并不一定意味著哪里出錯(cuò)了。它可能只是代表著程序如果想用一個(gè)文件得事先創(chuàng)建它。
與之相反,程序員失誤是徹徹底底的 Bug。這些情形下你會(huì)犯錯(cuò):忘記驗(yàn)證用戶輸入,敲錯(cuò)了變量名,諸如此類(lèi)。這樣的錯(cuò)誤根本就沒(méi)法被處理,如果可以,那就意味著你用處理錯(cuò)誤的代碼代替了出錯(cuò)的代碼。
這樣的區(qū)分很重要:操作失敗是程序正常操作的一部分。而由程序員的失誤則是 Bug。
有的時(shí)候,你會(huì)在一個(gè) Root 問(wèn)題里同時(shí)遇到操作失敗和程序員的失誤。HTTP 服務(wù)器訪問(wèn)了未定義的變量時(shí)奔潰了,這是程序員的失誤。當(dāng)前連接著的客戶端會(huì)在程序崩潰的同時(shí)看到一個(gè)ECONNRESET錯(cuò)誤,在 NodeJS 里通常會(huì)被報(bào)成“Socket Hang-up”。對(duì)客戶端來(lái)說(shuō),這是一個(gè)不相關(guān)的操作失敗, 那是因?yàn)檎_的客戶端必須處理服務(wù)器宕機(jī)或者網(wǎng)絡(luò)中斷的情況。
類(lèi)似的,如果不處理好操作失敗, 這本身就是一個(gè)失誤。舉個(gè)例子,如果程序想要連接服務(wù)器,但是得到一個(gè) ECONNREFUSED 錯(cuò)誤,而這個(gè)程序沒(méi)有監(jiān)聽(tīng)套接字上的error事件,然后程序崩潰了,這是程序員的失誤。連接斷開(kāi)是操作失?。ㄒ?yàn)檫@是任何一個(gè)正確的程序在系統(tǒng)的網(wǎng)絡(luò)或者其它模塊出問(wèn)題時(shí)都會(huì)經(jīng)歷的),如果它不被正確處理,那它就是一個(gè)失誤。
理解操作失敗和程序員失誤的不同, 是搞清怎么傳遞異常和處理異常的基礎(chǔ)。明白了這點(diǎn)再繼續(xù)往下讀。
處理操作失敗
就像性能和安全問(wèn)題一樣,錯(cuò)誤處理并不是可以憑空加到一個(gè)沒(méi)有任何錯(cuò)誤處理的程序中的。你沒(méi)有辦法在一個(gè)集中的地方處理所有的異常,就像你不能在一個(gè)集中的地方解決所有的性能問(wèn)題。你得考慮任何會(huì)導(dǎo)致失敗的代碼(比如打開(kāi)文件,連接服務(wù)器,F(xiàn)ork 子進(jìn)程等)可能產(chǎn)生的結(jié)果。包括為什么出錯(cuò),錯(cuò)誤背后的原因。之后會(huì)提及,但是關(guān)鍵在于錯(cuò)誤處理的粒度要細(xì),因?yàn)槟睦锍鲥e(cuò)和為什么出錯(cuò)決定了影響大小和對(duì)策。
你可能會(huì)發(fā)現(xiàn)在棧的某幾層不斷地處理相同的錯(cuò)誤。這是因?yàn)榈讓映讼蛏蠈觽鬟f錯(cuò)誤,上層再向它的上層傳遞錯(cuò)誤以外,底層沒(méi)有做任何有意義的事情。通常,只有頂層的調(diào)用者知道正確的應(yīng)對(duì)是什么,是重試操作,報(bào)告給用戶還是其它。但是那并不意味著,你應(yīng)該把所有的錯(cuò)誤全都丟給頂層的回調(diào)函數(shù)。因?yàn)?,頂層的回調(diào)函數(shù)不知道發(fā)生錯(cuò)誤的上下文,不知道哪些操作已經(jīng)成功執(zhí)行,哪些操作實(shí)際上失敗了。
我們來(lái)更具體一些。對(duì)于一個(gè)給定的錯(cuò)誤,你可以做這些事情:
- 直接處理。有的時(shí)候該做什么很清楚。如果你在嘗試打開(kāi)日志文件的時(shí)候得到了一個(gè)ENOENT錯(cuò)誤,很有可能你是第一次打開(kāi)這個(gè)文件,你要做的就是首先創(chuàng)建它。有的時(shí)候該做什么很清楚。如果你在嘗試打開(kāi)日志文件的時(shí)候得到了一個(gè)ENOENT錯(cuò)誤,很有可能你是第一次打開(kāi)這個(gè)文件,你要做的就是首先創(chuàng)建它。
- 把出錯(cuò)擴(kuò)散到客戶端。如果你不知道怎么處理這個(gè)異常,最簡(jiǎn)單的方式就是放棄你正在執(zhí)行的操作,清理所有開(kāi)始的,然后把錯(cuò)誤傳遞給客戶端。(怎么傳遞異常是另外一回事了,接下來(lái)會(huì)討論)。這種方式適合錯(cuò)誤短時(shí)間內(nèi)無(wú)法解決的情形。比如,用戶提交了不正確的JSON,你再解析一次是沒(méi)什么幫助的。
- 重試操作。對(duì)于那些來(lái)自網(wǎng)絡(luò)和遠(yuǎn)程服務(wù)的錯(cuò)誤,有的時(shí)候重試操作就可以解決問(wèn)題。比如,遠(yuǎn)程服務(wù)返回了503(服務(wù)不可用錯(cuò)誤),你可能會(huì)在幾秒種后重試。**如果確定要重試,你應(yīng)該清晰的用文檔記錄下將會(huì)多次重試,重試多少次直到失敗,以及兩次重試的間隔。 **另外,不要每次都假設(shè)需要重試。如果在棧中很深的地方(比如,被一個(gè)客戶端調(diào)用,而那個(gè)客戶端被另外一個(gè)由用戶操作的客戶端控制),這種情形下快速失敗讓客戶端去重試會(huì)更好。如果棧中的每一層都覺(jué)得需要重試,用戶最終會(huì)等待更長(zhǎng)的時(shí)間,因?yàn)槊恳粚佣紱](méi)有意識(shí)到下層同時(shí)也在嘗試。
- 直接崩潰。對(duì)于那些本不可能發(fā)生的錯(cuò)誤,或者由程序員失誤導(dǎo)致的錯(cuò)誤(比如無(wú)法連接到同一程序里的本地套接字),可以記錄一個(gè)錯(cuò)誤日志然后直接崩潰。其它的比如內(nèi)存不足這種錯(cuò)誤,是JavaScript這樣的腳本語(yǔ)言無(wú)法處理的,崩潰是十分合理的。(即便如此,在child_process.exec這樣的分離的操作里,得到ENOMEM錯(cuò)誤,或者那些你可以合理處理的錯(cuò)誤時(shí),你應(yīng)該考慮這么做)。在你無(wú)計(jì)可施需要讓管理員做修復(fù)的時(shí)候,你也可以直接崩潰。如果你用光了所有的文件描述符或者沒(méi)有訪問(wèn)配置文件的權(quán)限,這種情況下你什么都做不了,只能等某個(gè)用戶登錄系統(tǒng)把東西修好。
- 記錄錯(cuò)誤,其他什么都不做。有的時(shí)候你什么都做不了,沒(méi)有操作可以重試或者放棄,沒(méi)有任何理由崩潰掉應(yīng)用程序。舉個(gè)例子吧,你用DNS跟蹤了一組遠(yuǎn)程服務(wù),結(jié)果有一個(gè)DNS失敗了。除了記錄一條日志并且繼續(xù)使用剩下的服務(wù)以外,你什么都做不了。但是,你至少得記錄點(diǎn)什么(凡事都有例外。如果這種情況每秒發(fā)生幾千次,而你又沒(méi)法處理,那每次發(fā)生都記錄可能就不值得了,但是要周期性的記錄)。
(沒(méi)有辦法) 處理程序員的失誤
對(duì)于程序員的失誤沒(méi)有什么好做的。從定義上看,一段本該工作的代碼壞掉了(比如變量名敲錯(cuò)),你不能用更多的代碼再去修復(fù)它。一旦你這樣做了,你就使用錯(cuò)誤處理的代碼代替了出錯(cuò)的代碼。
有些人贊成從程序員的失誤中恢復(fù),也就是讓當(dāng)前的操作失敗,但是繼續(xù)處理請(qǐng)求。這種做法不推薦??紤]這樣的情況:原始代碼里有一個(gè)失誤是沒(méi)考慮到某種特殊情況。你怎么確定這個(gè)問(wèn)題不會(huì)影響其他請(qǐng)求呢?如果其它的請(qǐng)求共享了某個(gè)狀態(tài)(服務(wù)器,套接字,數(shù)據(jù)庫(kù)連接池等),有極大的可能其他請(qǐng)求會(huì)不正常。
典型的例子是 REST 服務(wù)器(比如用Restify搭的),如果有一個(gè)請(qǐng)求處理函數(shù)拋出了一個(gè)ReferenceError(比如,變量名打錯(cuò))。繼續(xù)運(yùn)行下去很有肯能會(huì)導(dǎo)致嚴(yán)重的 Bug,而且極其難發(fā)現(xiàn)。例如:
- 一些請(qǐng)求間共享的狀態(tài)可能會(huì)被變成null,undefined或者其它無(wú)效值,結(jié)果就是下一個(gè)請(qǐng)求也失敗了。
- 數(shù)據(jù)庫(kù)(或其它)連接可能會(huì)被泄露,降低了能夠并行處理的請(qǐng)求數(shù)量。最后只剩下幾個(gè)可用連接會(huì)很壞,將導(dǎo)致請(qǐng)求由并行變成串行被處理。
- 更糟的是, postgres 連接會(huì)被留在打開(kāi)的請(qǐng)求事務(wù)里。這會(huì)導(dǎo)致 postgres “持有”表中某一行的舊值,因?yàn)樗鼘?duì)這個(gè)事務(wù)可見(jiàn)。這個(gè)問(wèn)題會(huì)存在好幾周,造成表無(wú)限制的增長(zhǎng),后續(xù)的請(qǐng)求全都被拖慢了,從幾毫秒到幾分鐘[腳注4]。雖然這個(gè)問(wèn)題和 postgres 緊密相關(guān),但是它很好的說(shuō)明了程序員一個(gè)簡(jiǎn)單的失誤會(huì)讓?xiě)?yīng)用程序陷入一種非常可怕的狀態(tài)。
- 連接會(huì)停留在已認(rèn)證的狀態(tài),并且被后續(xù)的連接使用。結(jié)果就是在請(qǐng)求里搞錯(cuò)了用戶。
- 套接字會(huì)一直打開(kāi)著。一般情況下NodeJS 會(huì)在一個(gè)空閑的套接字上應(yīng)用兩分鐘的超時(shí),但這個(gè)值可以覆蓋,這將會(huì)泄露一個(gè)文件描述符。如果這種情況不斷發(fā)生,程序會(huì)因?yàn)橛霉饬怂械奈募枋龇鴱?qiáng)退。即使不覆蓋這個(gè)超時(shí)時(shí)間,客戶端會(huì)掛兩分鐘直到 “hang-up” 錯(cuò)誤的發(fā)生。這兩分鐘的延遲會(huì)讓問(wèn)題難于處理和調(diào)試。
- 很多內(nèi)存引用會(huì)被遺留。這會(huì)導(dǎo)致泄露,進(jìn)而導(dǎo)致內(nèi)存耗盡,GC 需要的時(shí)間增加,最后性能急劇下降。這點(diǎn)非常難調(diào)試,而且很需要技巧與導(dǎo)致造成泄露的失誤聯(lián)系起來(lái)。
最好的從失誤恢復(fù)的方法是立刻崩潰。你應(yīng)該用一個(gè)restarter 來(lái)啟動(dòng)你的程序,在奔潰的時(shí)候自動(dòng)重啟。如果restarter 準(zhǔn)備就緒,崩潰是失誤來(lái)臨時(shí)最快的恢復(fù)可靠服務(wù)的方法。
奔潰應(yīng)用程序唯一的負(fù)面影響是相連的客戶端臨時(shí)被擾亂,但是記住:
- 從定義上看,這些錯(cuò)誤屬于 Bug。我們并不是在討論正常的系統(tǒng)或是網(wǎng)絡(luò)錯(cuò)誤,而是程序里實(shí)際存在的Bug。它們應(yīng)該在線上很罕見(jiàn),并且是調(diào)試和修復(fù)的最高優(yōu)先級(jí)。
- 上面討論的種種情形里,請(qǐng)求沒(méi)有必要一定得成功完成。請(qǐng)求可能成功完成,可能讓服務(wù)器再次崩潰,可能以某種明顯的方式不正確的完成,或者以一種很難調(diào)試的方式錯(cuò)誤的結(jié)束了。
- 在一個(gè)完備的分布式系統(tǒng)里,客戶端必須能夠通過(guò)重連和重試來(lái)處理服務(wù)端的錯(cuò)誤。不管 NodeJS 應(yīng)用程序是否被允許崩潰,網(wǎng)絡(luò)和系統(tǒng)的失敗已經(jīng)是一個(gè)事實(shí)了。
- 如果你的線上代碼如此頻繁地崩潰讓連接斷開(kāi)變成了問(wèn)題,那么正真的問(wèn)題是你的服務(wù)器 Bug 太多了,而不是因?yàn)槟氵x擇出錯(cuò)就崩潰。
如果出現(xiàn)服務(wù)器經(jīng)常崩潰導(dǎo)致客戶端頻繁掉線的問(wèn)題,你應(yīng)該把經(jīng)歷集中在造成服務(wù)器崩潰的 Bug 上,把它們變成可捕獲的異常,而不是在代碼明顯有問(wèn)題的情況下盡可能地避免崩潰。調(diào)試這類(lèi)問(wèn)題最好的方法是,把 NodeJS 配置成出現(xiàn)未捕獲異常時(shí)把內(nèi)核文件打印出來(lái)。在 GNU/Linux 或者 基于 illumos 的系統(tǒng)上使用這些內(nèi)核文件,你不僅查看應(yīng)用崩潰時(shí)的堆棧記錄,還可以看到傳遞給函數(shù)的參數(shù)和其它的 JavaScript 對(duì)象,甚至是那些在閉包里引用的變量。即使沒(méi)有配置 code dumps,你也可以用堆棧信息和日志來(lái)開(kāi)始處理問(wèn)題。
最后,記住程序員在服務(wù)器端的失誤會(huì)造成客戶端的操作失敗,還有客戶端必須處理好服務(wù)器端的奔潰和網(wǎng)絡(luò)中斷。這不只是理論,而是實(shí)際發(fā)生在線上環(huán)境里。
編寫(xiě)函數(shù)的實(shí)踐
我們已經(jīng)討論了如何處理異常,那么當(dāng)你在編寫(xiě)新的函數(shù)的時(shí)候,怎么才能向調(diào)用者傳遞錯(cuò)誤呢?
最最重要的一點(diǎn)是為你的函數(shù)寫(xiě)好文檔,包括它接受的參數(shù)(附上類(lèi)型和其它約束),返回值,可能發(fā)生的錯(cuò)誤,以及這些錯(cuò)誤意味著什么。如果你不知道會(huì)導(dǎo)致什么錯(cuò)誤或者不了解錯(cuò)誤的含義,那你的應(yīng)用程序正常工作就是一個(gè)巧合。所以,當(dāng)你編寫(xiě)新的函數(shù)的時(shí)候,一定要告訴調(diào)用者可能發(fā)生哪些錯(cuò)誤和錯(cuò)誤的含義。
Throw, Callback 還是 EventEmitter
函數(shù)有三種基本的傳遞錯(cuò)誤的模式。
- throw以同步的方式傳遞異常–也就是在函數(shù)被調(diào)用處的相同的上下文。如果調(diào)用者(或者調(diào)用者的調(diào)用者)用了try/catch,則異??梢圆东@。如果所有的調(diào)用者都沒(méi)有用,那么程序通常情況下會(huì)崩潰(異常也可能會(huì)被domains或者進(jìn)程級(jí)的uncaughtException捕捉到,詳見(jiàn)下文)。
- Callback是最基礎(chǔ)的異步傳遞事件的一種方式。用戶傳進(jìn)來(lái)一個(gè)函數(shù)(callback),之后當(dāng)某個(gè)異步操作完成后調(diào)用這個(gè) callback。通常 callback 會(huì)以callback(err,result)的形式被調(diào)用,這種情況下, err和 result必然有一個(gè)是非空的,取決于操作是成功還是失敗。
- 更復(fù)雜的情形是,函數(shù)沒(méi)有用 Callback 而是返回一個(gè) EventEmitter 對(duì)象,調(diào)用者需要監(jiān)聽(tīng)這個(gè)對(duì)象的 error事件。這種方式在兩種情況下很有用。
- 當(dāng)你在做一個(gè)可能會(huì)產(chǎn)生多個(gè)錯(cuò)誤或多個(gè)結(jié)果的復(fù)雜操作的時(shí)候。比如,有一個(gè)請(qǐng)求一邊從數(shù)據(jù)庫(kù)取數(shù)據(jù)一邊把數(shù)據(jù)發(fā)送回客戶端,而不是等待所有的結(jié)果一起到達(dá)。在這個(gè)例子里,沒(méi)有用 callback,而是返回了一個(gè) EventEmitter,每個(gè)結(jié)果會(huì)觸發(fā)一個(gè)row 事件,當(dāng)所有結(jié)果發(fā)送完畢后會(huì)觸發(fā)end事件,出現(xiàn)錯(cuò)誤時(shí)會(huì)觸發(fā)一個(gè)error事件。
用在那些具有復(fù)雜狀態(tài)機(jī)的對(duì)象上,這些對(duì)象往往伴隨著大量的異步事件。例如,一個(gè)套接字是一個(gè)EventEmitter,它可能會(huì)觸發(fā)“connect“,”end“,”timeout“,”drain“,”close“事件。這樣,很自然地可以把”error“作為另外一種可以被觸發(fā)的事件。在這種情況下,清楚知道”error“還有其它事件何時(shí)被觸發(fā)很重要,同時(shí)被觸發(fā)的還有什么事件(例如”close“),觸發(fā)的順序,還有套接字是否在結(jié)束的時(shí)候處于關(guān)閉狀態(tài)。
在大多數(shù)情況下,我們會(huì)把 callback 和 event emitter 歸到同一個(gè)“異步錯(cuò)誤傳遞”籃子里。如果你有傳遞異步錯(cuò)誤的需要,你通常只要用其中的一種而不是同時(shí)使用。
那么,什么時(shí)候用throw,什么時(shí)候用callback,什么時(shí)候又用 EventEmitter 呢?這取決于兩件事:
- 這是操作失敗還是程序員的失誤?
- 這個(gè)函數(shù)本身是同步的還是異步的?
直到目前,最常見(jiàn)的例子是在異步函數(shù)里發(fā)生了操作失敗。在大多數(shù)情況下,你需要寫(xiě)一個(gè)以回調(diào)函數(shù)作為參數(shù)的函數(shù),然后你會(huì)把異常傳遞給這個(gè)回調(diào)函數(shù)。這種方式工作的很好,并且被廣泛使用。例子可參照 NodeJS 的 fs 模塊。如果你的場(chǎng)景比上面這個(gè)還復(fù)雜,那么你可能就得換用 EventEmitter 了,不過(guò)你也還是在用異步方式傳遞這個(gè)錯(cuò)誤。
其次常見(jiàn)的一個(gè)例子是像JSON.parse 這樣的函數(shù)同步產(chǎn)生了一個(gè)異常。對(duì)這些函數(shù)而言,如果遇到操作失?。ū热鐭o(wú)效輸入),你得用同步的方式傳遞它。你可以拋出(更加常見(jiàn))或者返回它。
對(duì)于給定的函數(shù),如果有一個(gè)異步傳遞的異常,那么所有的異常都應(yīng)該被異步傳遞??赡苡羞@樣的情況,請(qǐng)求一到來(lái)你就知道它會(huì)失敗,并且知道不是因?yàn)槌绦騿T的失誤。可能的情形是你緩存了返回給最近請(qǐng)求的錯(cuò)誤。雖然你知道請(qǐng)求一定失敗,但是你還是應(yīng)該用異步的方式傳遞它。
通用的準(zhǔn)則就是你即可以同步傳遞錯(cuò)誤(拋出),也可以異步傳遞錯(cuò)誤(通過(guò)傳給一個(gè)回調(diào)函數(shù)或者觸發(fā) EventEmitter 的 error事件),但是不用同時(shí)使用。以這種方式,用戶處理異常的時(shí)候可以選擇用回調(diào)函數(shù)還是用try/catch,但是不需要兩種都用。具體用哪一個(gè)取決于異常是怎么傳遞的,這點(diǎn)得在文檔里說(shuō)明清楚。
差點(diǎn)忘了程序員的失誤?;貞浺幌?,它們其實(shí)是 Bug 。在函數(shù)開(kāi)頭通過(guò)檢查參數(shù)的類(lèi)型(或是其它約束)就可以被立即發(fā)現(xiàn)。一個(gè)退化的例子是,某人調(diào)用了一個(gè)異步的函數(shù),但是沒(méi)有傳回調(diào)函數(shù)。你應(yīng)該立刻把這個(gè)錯(cuò)拋出,因?yàn)槌绦蛞呀?jīng)出錯(cuò)而在這個(gè)點(diǎn)上最好的調(diào)試的機(jī)會(huì)就是得到一個(gè)堆棧信息,如果有內(nèi)核信息就更好了。
因?yàn)槌绦騿T的失誤永遠(yuǎn)不應(yīng)該被處理,上面提到的調(diào)用者只能用 try/catch 或者回調(diào)函數(shù)(或者 EventEmitter)其中一種處理異常的準(zhǔn)則并沒(méi)有因?yàn)檫@條意見(jiàn)而改變。如果你想知道更多,請(qǐng)見(jiàn)上面的 (不要)處理程序員的失誤。
下表以 NodeJS 核心模塊的常見(jiàn)函數(shù)為例,做了一個(gè)總結(jié),大致按照每種問(wèn)題出現(xiàn)的頻率來(lái)排列:

異步函數(shù)里出現(xiàn)操作錯(cuò)誤的例子(第一行)是最常見(jiàn)的。在同步函數(shù)里發(fā)生操作失?。ǖ诙校┍容^少見(jiàn),除非是驗(yàn)證用戶輸入。程序員失誤(第三行)除非是在開(kāi)發(fā)環(huán)境下,否則永遠(yuǎn)都不應(yīng)該出現(xiàn)。
吐槽:程序員失誤還是操作失???
你怎么知道是程序員的失誤還是操作失敗呢?很簡(jiǎn)單,你自己來(lái)定義并且記在文檔里,包括允許什么類(lèi)型的函數(shù),怎樣打斷它的執(zhí)行。如果你得到的異常不是文檔里能接受的,那就是一個(gè)程序員失誤。如果在文檔里寫(xiě)明接受但是暫時(shí)處理不了的,那就是一個(gè)操作失敗。
你得用你的判斷力去決定你想做到多嚴(yán)格,但是我們會(huì)給你一定的意見(jiàn)。具體一些,想象有個(gè)函數(shù)叫做“connect”,它接受一個(gè)IP地址和一個(gè)回調(diào)函數(shù)作為參數(shù),這個(gè)回調(diào)函數(shù)會(huì)在成功或者失敗的時(shí)候被調(diào)用。現(xiàn)在假設(shè)用戶傳進(jìn)來(lái)一個(gè)明顯不是IP地址的參數(shù),比如“bob”,這個(gè)時(shí)候你有幾種選擇:
- 在文檔里寫(xiě)清楚只接受有效的IPV4的地址,當(dāng)用戶傳進(jìn)來(lái)“bob”的時(shí)候拋出一個(gè)異常。強(qiáng)烈推薦這種做法。
- 在文檔里寫(xiě)上接受任何string類(lèi)型的參數(shù)。如果用戶傳的是“bob”,觸發(fā)一個(gè)異步錯(cuò)誤指明無(wú)法連接到“bob”這個(gè)IP地址。
這兩種方式和我們上面提到的關(guān)于操作失敗和程序員失誤的指導(dǎo)原則是一致的。你決定了這樣的輸入算是程序員的失誤還是操作失敗。通常,用戶輸入的校驗(yàn)是很松的,為了證明這點(diǎn),可以看Date.parse這個(gè)例子,它接受很多類(lèi)型的輸入。但是對(duì)于大多數(shù)其它函數(shù),我們強(qiáng)烈建議你偏向更嚴(yán)格而不是更松。你的程序越是猜測(cè)用戶的本意(使用隱式的轉(zhuǎn)換,無(wú)論是JavaScript語(yǔ)言本身這么做還是有意為之),就越是容易猜錯(cuò)。本意是想讓開(kāi)發(fā)者在使用的時(shí)候不用更加具體,結(jié)果卻耗費(fèi)了人家好幾個(gè)小時(shí)在Debug上。再說(shuō)了,如果你覺(jué)得這是個(gè)好主意,你也可以在未來(lái)的版本里讓函數(shù)不那么嚴(yán)格,但是如果你發(fā)現(xiàn)由于猜測(cè)用戶的意圖導(dǎo)致了很多惱人的bug,要修復(fù)它的時(shí)候想保持兼容性就不大可能了。
所以如果一個(gè)值怎么都不可能是有效的(本該是string卻得到一個(gè)undefined,本該是string類(lèi)型的IP但明顯不是),你應(yīng)該在文檔里寫(xiě)明是這不允許的并且立刻拋出一個(gè)異常。只要你在文檔里寫(xiě)的清清楚楚,那這就是一個(gè)程序員的失誤而不是操作失敗。立即拋出可以把Bug帶來(lái)的損失降到最小,并且保存了開(kāi)發(fā)者可以用來(lái)調(diào)試這個(gè)問(wèn)題的信息(例如,調(diào)用堆棧,如果用內(nèi)核文件還可以得到參數(shù)和內(nèi)存分布)。
那么 domains 和 process.on('uncaughtException') 呢?
操作失敗總是可以被顯示的機(jī)制所處理的:捕獲一個(gè)異常,在回調(diào)里處理錯(cuò)誤,或者處理 EventEmitter 的“error”事件等等。Domains以及進(jìn)程級(jí)別的‘uncaughtException’主要是用來(lái)從未料到的程序錯(cuò)誤恢復(fù)的。由于上面我們所討論的原因,這兩種方式都不鼓勵(lì)。
編寫(xiě)新函數(shù)的具體建議
我們已經(jīng)談?wù)摿撕芏嘀笇?dǎo)原則,現(xiàn)在讓我們具體一些。
你的函數(shù)做什么得很清楚。 這點(diǎn)非常重要。每個(gè)接口函數(shù)的文檔都要很清晰的說(shuō)明: - 預(yù)期參數(shù) - 參數(shù)的類(lèi)型 - 參數(shù)的額外約束(例如,必須是有效的IP地址) 如果其中有一點(diǎn)不正確或者缺少,那就是一個(gè)程序員的失誤,你應(yīng)該立刻拋出來(lái)。 此外,你還要記錄:
- 調(diào)用者可能會(huì)遇到的操作失?。ㄒ约八鼈兊?name)
- 怎么處理操作失敗(例如是拋出,傳給回調(diào)函數(shù),還是被 EventEmitter 發(fā)出)
- 返回值
- 使用 Error 對(duì)象或它的子類(lèi),并且實(shí)現(xiàn) Error 的協(xié)議。
你的所有錯(cuò)誤要么使用Error 類(lèi)要么使用它的子類(lèi)。你應(yīng)該提供name和message屬性,stack也是(注意準(zhǔn)確)。 - 在程序里通過(guò) Error 的 name屬性區(qū)分不同的錯(cuò)誤。
當(dāng)你想要知道錯(cuò)誤是何種類(lèi)型的時(shí)候,用 name 屬性。 JavaScript 內(nèi)置的供你重用的名字包括“RangeError”(參數(shù)超出有效范圍)和“TypeError”(參數(shù)類(lèi)型錯(cuò)誤)。而 HTTP 異常,通常會(huì)用 RFC 指定的名字,比如“BadRequestError”或者“ServiceUnavailableError”。 - 不要想著給每個(gè)東西都取一個(gè)新的名字。
如果你可以只用一個(gè)簡(jiǎn)單的 InvalidArgumentError,就不要分成 InvalidHostnameError,InvalidIpAddressError,InvalidDnsError等等,你要做的是通過(guò)增加屬性來(lái)說(shuō)明那里出了問(wèn)題(下面會(huì)講到)。 - 用詳細(xì)的屬性來(lái)增強(qiáng) Error 對(duì)象。
舉個(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ò)誤類(lèi)型(例如參數(shù)非法和連接失?。?br> message:一個(gè)供人類(lèi)閱讀的錯(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ò)誤輸入的字段)。
- 若果你傳遞一個(gè)底層的錯(cuò)誤給調(diào)用者,考慮先包裝一下。 經(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 開(kāi)始處理請(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è)新的名字)
- 要么用原有的名字,要么顯示地選擇一個(gè)更有意義的名字。例如,最底層是 NodeJS 報(bào)的一個(gè)簡(jiǎn)單的Error,但在步驟1中可以是個(gè) IntializationError 。(但是如果程序可以通過(guò)其它的屬性區(qū)分,不要覺(jué)得有責(zé)任取一個(gè)新的名字)
在 Joyent,我們使用verror 這個(gè)模塊來(lái)封裝錯(cuò)誤,因?yàn)樗恼Z(yǔ)法簡(jiǎn)潔。寫(xiě)這篇文章的時(shí)候,它還不能支持上面的所有功能,但是會(huì)被擴(kuò)展以期支持。
例子
考慮有這樣的一個(gè)函數(shù),這個(gè)函數(shù)會(huì)異步地連接到一個(gè) IPv4 地址的 TCP 端口。我們通過(guò)例子來(lái)看文檔怎么寫(xiě):
/*
* Make a TCP connection to the given IPv4 address. Arguments:
*
* ip4addr a string representing a valid IPv4 address
*
* tcpPort a positive integer representing a valid TCP port
*
* timeout a positive integer denoting the number of milliseconds
* to wait for a response from the remote server before
* considering the connection to have failed.
*
* callback invoked when the connection succeeds or fails. Upon
* success, callback is invoked as callback(null, socket),
* where `socket` is a Node net.Socket object. Upon failure,
* callback is invoked as callback(err) instead.
*
* This function may fail for several reasons:
*
* SystemError For "connection refused" and "host unreachable" and other
* errors returned by the connect(2) system call. For these
* errors, err.errno will be set to the actual errno symbolic
* name.
*
* TimeoutError Emitted if "timeout" milliseconds elapse without
* successfully completing the connection.
*
* All errors will have the conventional "remoteIp" and "remotePort" properties.
* After any error, any socket that was created will be closed.
*/
function connect(ip4addr, tcpPort, timeout, callback)
{
assert.equal(typeof (ip4addr), 'string',
"argument 'ip4addr' must be a string");
assert.ok(net.isIPv4(ip4addr),
"argument 'ip4addr' must be a valid IPv4 address");
assert.equal(typeof (tcpPort), 'number',
"argument 'tcpPort' must be a number");
assert.ok(!isNaN(tcpPort) && tcpPort > 0 && tcpPort < 65536,
"argument 'tcpPort' must be a positive integer between 1 and 65535");
assert.equal(typeof (timeout), 'number',
"argument 'timeout' must be a number");
assert.ok(!isNaN(timeout) && timeout > 0,
"argument 'timeout' must be a positive integer");
assert.equal(typeof (callback), 'function');
/* do work */
}
這個(gè)例子在概念上很簡(jiǎn)單,但是展示了上面我們所談?wù)摰囊恍┙ㄗh:
- 參數(shù),類(lèi)型以及其它一些約束被清晰的文檔化。
- 這個(gè)函數(shù)對(duì)于接受的參數(shù)是非常嚴(yán)格的,并且會(huì)在得到錯(cuò)誤參數(shù)的時(shí)候拋出異常(程序員的失誤)。
- 可能出現(xiàn)的操作失敗集合被記錄了。通過(guò)不同的”name“值可以區(qū)分不同的異常,而”errno“被用來(lái)獲得系統(tǒng)錯(cuò)誤的詳細(xì)信息。
- 異常被傳遞的方式也被記錄了(通過(guò)失敗時(shí)調(diào)用回調(diào)函數(shù))。
- 返回的錯(cuò)誤有”remoteIp“和”remotePort“字段,這樣用戶就可以定義自己的錯(cuò)誤了(比如,一個(gè)HTTP客戶端的端口號(hào)是隱含的)。
- 雖然很明顯,但是連接失敗后的狀態(tài)也被清晰的記錄了:所有被打開(kāi)的套接字此時(shí)已經(jīng)被關(guān)閉。
這看起來(lái)像是給一個(gè)很容易理解的函數(shù)寫(xiě)了超過(guò)大部分人會(huì)寫(xiě)的的超長(zhǎng)注釋?zhuān)蟛糠趾瘮?shù)實(shí)際上沒(méi)有這么容易理解。所有建議都應(yīng)該被有選擇的吸收,如果事情很簡(jiǎn)單,你應(yīng)該自己做出判斷,但是記?。河檬昼姲杨A(yù)計(jì)發(fā)生的記錄下來(lái)可能之后會(huì)為你或其他人節(jié)省數(shù)個(gè)小時(shí)。
總結(jié)
- 學(xué)習(xí)了怎么區(qū)分操作失敗,即那些可以被預(yù)測(cè)的哪怕在正確的程序里也無(wú)法避免的錯(cuò)誤(例如,無(wú)法連接到服務(wù)器);而程序的Bug則是程序員失誤。
- 操作失敗可以被處理,也應(yīng)當(dāng)被處理。程序員的失誤無(wú)法被處理或可靠地恢復(fù)(本不應(yīng)該這么做),嘗試這么做只會(huì)讓問(wèn)題更難調(diào)試。
- 一個(gè)給定的函數(shù),它處理異常的方式要么是同步(用 throw方式)要么是異步的(用 callback 或者 EventEmitter),不會(huì)兩者兼具。用戶可以在回調(diào)函數(shù)里處理錯(cuò)誤,也可以使用 try/catch捕獲異常 ,但是不能一起用。實(shí)際上,使用throw并且期望調(diào)用者使用 try/catch 是很罕見(jiàn)的,因?yàn)?NodeJS里的同步函數(shù)通常不會(huì)產(chǎn)生運(yùn)行失?。ㄖ饕睦馐穷?lèi)似于JSON.parse的用戶輸入驗(yàn)證函數(shù))。
- 在寫(xiě)新函數(shù)的時(shí)候,用文檔清楚地記錄函數(shù)預(yù)期的參數(shù),包括它們的類(lèi)型、是否有其它約束(例如必須是有效的 IP 地址),可能會(huì)發(fā)生的合理的操作失?。ɡ鐭o(wú)法解析主機(jī)名,連接服務(wù)器失敗,所有的服務(wù)器端錯(cuò)誤),錯(cuò)誤是怎么傳遞給調(diào)用者的(同步,用throw,還是異步,用 callback 和 EventEmitter)。
- 缺少參數(shù)或者參數(shù)無(wú)效是程序員的失誤,一旦發(fā)生總是應(yīng)該拋出異常。函數(shù)的作者認(rèn)為的可接受的參數(shù)可能會(huì)有一個(gè)灰色地帶,但是如果傳遞的是一個(gè)文檔里寫(xiě)明接收的參數(shù)以外的東西,那就是一個(gè)程序員失誤。
- 傳遞錯(cuò)誤的時(shí)候用標(biāo)準(zhǔn)的 Error 類(lèi)和它標(biāo)準(zhǔn)的屬性。盡可能把額外的有用信息放在對(duì)應(yīng)的屬性里。如果有可能,用約定的屬性名(如下)。
附錄:Error 對(duì)象屬性命名約定
強(qiáng)烈建議你在發(fā)生錯(cuò)誤的時(shí)候用這些名字來(lái)保持和 Node 核心以及 Node 插件的一致。這些大部分不會(huì)和某個(gè)給定的異常對(duì)應(yīng),但是出現(xiàn)疑問(wèn)的時(shí)候,你應(yīng)該包含任何看起來(lái)有用的信息,即從編程上也從自定義的錯(cuò)誤消息上。

腳注
- 人們有的時(shí)候會(huì)這么寫(xiě)代碼,他們想要在出現(xiàn)異步錯(cuò)誤的時(shí)候調(diào)用callback 并把錯(cuò)誤作為參數(shù)傳遞。他們錯(cuò)誤地認(rèn)為在自己的回調(diào)函數(shù)(傳遞給 doSomeAsynchronousOperation 的函數(shù))里throw 一個(gè)異常,會(huì)被外面的 catch 代碼塊捕獲。try/catch和異步函數(shù)不是這么工作的。回憶一下,異步函數(shù)的意義就在于被調(diào)用的時(shí)候myApiFunc函數(shù)已經(jīng)返回了。這意味著 try 代碼塊已經(jīng)退出了。這個(gè)回調(diào)函數(shù)是由 Node 直接調(diào)用的,外面并沒(méi)有 try 的代碼塊。如果你用這個(gè)反模式,結(jié)果就是拋出異常的時(shí)候,程序崩潰了。
- 在 JavaScript 里,拋出一個(gè)不屬于Error的參數(shù)從技術(shù)上是可行的,但是應(yīng)該被避免。這樣的結(jié)果使獲得調(diào)用堆棧沒(méi)有可能,代碼也無(wú)法檢查name屬性,或者其它任何能夠說(shuō)明哪里有問(wèn)題的屬性。
- 操作失敗和程序員的失誤這一概念早在 NodeJS 之前就已經(jīng)存在存在了。不嚴(yán)格地對(duì)應(yīng)者 Java 里的 checked 和 unchecked 異常,雖然操作失敗被認(rèn)為是無(wú)法避免的,比如 OutOfMemeoryError,被歸為 uncheked 異常。在 C 語(yǔ)言里有對(duì)應(yīng)的概念,普通異常處理和使用斷言。維基百科上關(guān)于斷言的的文章也有關(guān)于什么時(shí)候用斷言什么時(shí)候用普通的錯(cuò)誤處理的類(lèi)似的解釋。
- 如果這看起來(lái)非常具體,那是因?yàn)槲覀冊(cè)诋a(chǎn)品環(huán)境中遇到這樣過(guò)這樣的問(wèn)題。這真的很可怕。