NodeJS 的錯(cuò)誤處理讓人痛苦,在很長的一段時(shí)間里撑毛,大量的錯(cuò)誤被放任不管书聚。但是要想建立一個(gè)健壯的 Node.js 程序就必須正確的處理這些錯(cuò)誤,而且這并不難學(xué)代态。如果你實(shí)在沒有耐心寺惫,那就直接繞過長篇大論跳到“總結(jié)”部分吧。
英文原文:https://www.joyent.com/node-js/production/design/errors
這篇文章會(huì)回答 NodeJS 初學(xué)者的若干問題:
- 我寫的函數(shù)里什么時(shí)候該拋出異常蹦疑,什么時(shí)候該傳給 callback,什么時(shí)候觸發(fā) EventEmitter 等等西雀。
- 我的函數(shù)對參數(shù)該做出怎樣的假設(shè)?我應(yīng)該檢查更加具體的約束么歉摧?例如參數(shù)是否非空艇肴,是否大于零,是不是看起來像個(gè) IP 地址叁温,等等等再悼。
- 我該如何處理那些不符合預(yù)期的參數(shù)?我是應(yīng)該拋出一個(gè)異常膝但,還是把錯(cuò)誤傳遞給一個(gè) callback冲九。
- 我該怎么在程序里區(qū)分不同的異常(比如“請求錯(cuò)誤”和“服務(wù)不可用”)?
- 我怎么才能提供足夠的信息讓調(diào)用者知曉錯(cuò)誤細(xì)節(jié)跟束。
- 我該怎么處理未預(yù)料的出錯(cuò)莺奸?我是應(yīng)該用 try/catch ,domains 還是其它什么方式呢冀宴?
這篇文章可以劃分成互相為基礎(chǔ)的幾個(gè)部分:
- 背景:希望你所具備的知識灭贷。
- 操作失敗和程序員的失誤:介紹兩種基本的異常。
- 編寫新函數(shù)的實(shí)踐:關(guān)于怎么讓函數(shù)產(chǎn)生有用報(bào)錯(cuò)的基本原則略贮。
- 編寫新函數(shù)的具體推薦:編寫能產(chǎn)生有用報(bào)錯(cuò)的甚疟、健壯的函數(shù)需要的一個(gè)檢查列表。
- 例子:以 connect 函數(shù)為例的文檔和序言逃延。
- 總結(jié):全文至此的觀點(diǎn)總結(jié)览妖。
- 附錄:Error 對象屬性約定:用標(biāo)準(zhǔn)方式提供一個(gè)屬性列表,以提供更多信息揽祥。
背景
本文假設(shè):
你已經(jīng)熟悉了 JavaScript黄痪、Java、 Python盔然、 C++ 或者類似的語言中異常的概念桅打,而且你知道拋出異常和捕獲異常是什么意思是嗜。
你熟悉怎么用 NodeJS 編寫代碼。你使用異步操作的時(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 事件魂挂。
接下來我們會(huì)詳細(xì)討論這幾種方式甫题。這篇文章不假設(shè)你知道任何關(guān)于 domains 的知識。
最后涂召,你應(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’));
這種用法更常見秋泳,因?yàn)樵?NodeJS 里潦闲,大部分的錯(cuò)誤都是異步的。實(shí)際上迫皱,try/catch 唯一常用的是在 JSON.parse 和類似驗(yàn)證用戶輸入的地方歉闰。接下來我們會(huì)看到,其實(shí)很少要捕獲一個(gè)異步函數(shù)里的異常卓起。這一點(diǎn)和 Java新娜,C++,以及其它嚴(yán)重依賴異常的語言很不一樣既绩。
操作失敗和程序員的失誤
把錯(cuò)誤分成兩大類很有用:
-
操作失敗是正確編寫的程序在運(yùn)行時(shí)產(chǎn)生的錯(cuò)誤。它并不是程序的 Bug还惠,反而經(jīng)常是其它問題:系統(tǒng)本身(內(nèi)存不足或者打開文件數(shù)過多)饲握,系統(tǒng)配置(沒有到達(dá)遠(yuǎn)程主機(jī)的路由),網(wǎng)絡(luò)問題(端口掛起)蚕键,遠(yuǎn)程服務(wù)(500錯(cuò)誤救欧,連接失敗)锣光。例子如下:
- 連接不到服務(wù)器
- 無法解析主機(jī)名
- 無效的用戶輸入
- 請求超時(shí)
- 服務(wù)器返回 500
- 套接字被掛起
- 系統(tǒng)內(nèi)存不足
-
程序員失誤是程序里的 Bug笆怠。這些錯(cuò)誤往往可以通過修改代碼避免。它們永遠(yuǎn)都沒法被有效的處理:
- 讀取 undefined 的一個(gè)屬性
- 調(diào)用異步函數(shù)沒有指定回調(diào)
- 該傳對象的時(shí)候傳了一個(gè)字符串
- 該傳IP地址的時(shí)候傳了一個(gè)對象
人們把操作失敗和程序員的失誤都稱為“錯(cuò)誤”誊爹,但其實(shí)它們很不一樣蹬刷。操作失敗是所有正確的程序應(yīng)該處理的錯(cuò)誤情形瓢捉,只要被妥善處理它們不一定會(huì)預(yù)示著 Bug 或是嚴(yán)重的問題“斐桑“文件找不到”是一個(gè)操作失敗泡态,但是它并不一定意味著哪里出錯(cuò)了。它可能只是代表著程序如果想用一個(gè)文件得事先創(chuàng)建它迂卢。
與之相反某弦,程序員失誤是徹徹底底的 Bug。這些情形下你會(huì)犯錯(cuò):忘記驗(yàn)證用戶輸入而克,敲錯(cuò)了變量名靶壮,諸如此類。這樣的錯(cuò)誤根本就沒法被處理员萍,如果可以腾降,那就意味著你用處理錯(cuò)誤的代碼代替了出錯(cuò)的代碼。
這樣的區(qū)分很重要:操作失敗是程序正常操作的一部分充活。而由程序員的失誤則是 Bug蜂莉。
有的時(shí)候,你會(huì)在一個(gè) Root 問題里同時(shí)遇到操作失敗和程序員的失誤混卵。HTTP 服務(wù)器訪問了未定義的變量時(shí)奔潰了映穗,這是程序員的失誤。當(dāng)前連接著的客戶端會(huì)在程序崩潰的同時(shí)看到一個(gè)ECONNRESET錯(cuò)誤幕随,在 NodeJS 里通常會(huì)被報(bào)成“Socket Hang-up”蚁滋。對客戶端來說,這是一個(gè)不相關(guān)的操作失敗, 那是因?yàn)檎_的客戶端必須處理服務(wù)器宕機(jī)或者網(wǎng)絡(luò)中斷的情況赘淮。
類似的辕录,如果不處理好操作失敗, 這本身就是一個(gè)失誤。舉個(gè)例子梢卸,如果程序想要連接服務(wù)器走诞,但是得到一個(gè) ECONNREFUSED 錯(cuò)誤,而這個(gè)程序沒有監(jiān)聽套接字上的error事件,然后程序崩潰了蛤高,這是程序員的失誤蚣旱。連接斷開是操作失敗(因?yàn)檫@是任何一個(gè)正確的程序在系統(tǒng)的網(wǎng)絡(luò)或者其它模塊出問題時(shí)都會(huì)經(jīng)歷的)戴陡,如果它不被正確處理塞绿,那它就是一個(gè)失誤。
理解操作失敗和程序員失誤的不同, 是搞清怎么傳遞異常和處理異常的基礎(chǔ)恤批。明白了這點(diǎn)再繼續(xù)往下讀异吻。
處理操作失敗
就像性能和安全問題一樣,錯(cuò)誤處理并不是可以憑空加到一個(gè)沒有任何錯(cuò)誤處理的程序中的喜庞。你沒有辦法在一個(gè)集中的地方處理所有的異常诀浪,就像你不能在一個(gè)集中的地方解決所有的性能問題棋返。你得考慮任何會(huì)導(dǎo)致失敗的代碼(比如打開文件,連接服務(wù)器笋妥,F(xiàn)ork 子進(jìn)程等)可能產(chǎn)生的結(jié)果懊昨。包括為什么出錯(cuò),錯(cuò)誤背后的原因春宣。之后會(huì)提及酵颁,但是關(guān)鍵在于錯(cuò)誤處理的粒度要細(xì),因?yàn)槟睦锍鲥e(cuò)和為什么出錯(cuò)決定了影響大小和對策月帝。
你可能會(huì)發(fā)現(xiàn)在棧的某幾層不斷地處理相同的錯(cuò)誤躏惋。這是因?yàn)榈讓映讼蛏蠈觽鬟f錯(cuò)誤,上層再向它的上層傳遞錯(cuò)誤以外嚷辅,底層沒有做任何有意義的事情簿姨。通常,只有頂層的調(diào)用者知道正確的應(yīng)對是什么簸搞,是重試操作扁位,報(bào)告給用戶還是其它。但是那并不意味著趁俊,你應(yīng)該把所有的錯(cuò)誤全都丟給頂層的回調(diào)函數(shù)域仇。因?yàn)椋攲拥幕卣{(diào)函數(shù)不知道發(fā)生錯(cuò)誤的上下文寺擂,不知道哪些操作已經(jīng)成功執(zhí)行暇务,哪些操作實(shí)際上失敗了。
我們來更具體一些怔软。對于一個(gè)給定的錯(cuò)誤垦细,你可以做這些事情:
- 直接處理。有的時(shí)候該做什么很清楚挡逼。如果你在嘗試打開日志文件的時(shí)候得到了一個(gè)ENOENT錯(cuò)誤括改,很有可能你是第一次打開這個(gè)文件,你要做的就是首先創(chuàng)建它家坎。有的時(shí)候該做什么很清楚嘱能。如果你在嘗試打開日志文件的時(shí)候得到了一個(gè)ENOENT錯(cuò)誤,很有可能你是第一次打開這個(gè)文件乘盖,你要做的就是首先創(chuàng)建它。
- 把出錯(cuò)擴(kuò)散到客戶端憔涉。如果你不知道怎么處理這個(gè)異常订框,最簡單的方式就是放棄你正在執(zhí)行的操作,清理所有開始的兜叨,然后把錯(cuò)誤傳遞給客戶端穿扳。(怎么傳遞異常是另外一回事了衩侥,接下來會(huì)討論)。這種方式適合錯(cuò)誤短時(shí)間內(nèi)無法解決的情形矛物。比如茫死,用戶提交了不正確的JSON,你再解析一次是沒什么幫助的履羞。
- 重試操作峦萎。對于那些來自網(wǎng)絡(luò)和遠(yuǎn)程服務(wù)的錯(cuò)誤,有的時(shí)候重試操作就可以解決問題忆首。比如爱榔,遠(yuǎn)程服務(wù)返回了503(服務(wù)不可用錯(cuò)誤),你可能會(huì)在幾秒種后重試糙及。**如果確定要重試详幽,你應(yīng)該清晰的用文檔記錄下將會(huì)多次重試,重試多少次直到失敗,以及兩次重試的間隔浸锨。 **另外唇聘,不要每次都假設(shè)需要重試。如果在棧中很深的地方(比如柱搜,被一個(gè)客戶端調(diào)用迟郎,而那個(gè)客戶端被另外一個(gè)由用戶操作的客戶端控制),這種情形下快速失敗讓客戶端去重試會(huì)更好冯凹。如果棧中的每一層都覺得需要重試谎亩,用戶最終會(huì)等待更長的時(shí)間,因?yàn)槊恳粚佣紱]有意識到下層同時(shí)也在嘗試宇姚。
- 直接崩潰匈庭。對于那些本不可能發(fā)生的錯(cuò)誤,或者由程序員失誤導(dǎo)致的錯(cuò)誤(比如無法連接到同一程序里的本地套接字)浑劳,可以記錄一個(gè)錯(cuò)誤日志然后直接崩潰阱持。其它的比如內(nèi)存不足這種錯(cuò)誤,是JavaScript這樣的腳本語言無法處理的魔熏,崩潰是十分合理的衷咽。(即便如此,在child_process.exec這樣的分離的操作里蒜绽,得到ENOMEM錯(cuò)誤镶骗,或者那些你可以合理處理的錯(cuò)誤時(shí),你應(yīng)該考慮這么做)躲雅。在你無計(jì)可施需要讓管理員做修復(fù)的時(shí)候鼎姊,你也可以直接崩潰。如果你用光了所有的文件描述符或者沒有訪問配置文件的權(quán)限,這種情況下你什么都做不了相寇,只能等某個(gè)用戶登錄系統(tǒng)把東西修好慰于。
- 記錄錯(cuò)誤,其他什么都不做唤衫。有的時(shí)候你什么都做不了婆赠,沒有操作可以重試或者放棄,沒有任何理由崩潰掉應(yīng)用程序佳励。舉個(gè)例子吧休里,你用DNS跟蹤了一組遠(yuǎn)程服務(wù),結(jié)果有一個(gè)DNS失敗了植兰。除了記錄一條日志并且繼續(xù)使用剩下的服務(wù)以外份帐,你什么都做不了。但是楣导,你至少得記錄點(diǎn)什么(凡事都有例外废境。如果這種情況每秒發(fā)生幾千次,而你又沒法處理筒繁,那每次發(fā)生都記錄可能就不值得了噩凹,但是要周期性的記錄)。
(沒有辦法) 處理程序員的失誤
對于程序員的失誤沒有什么好做的毡咏。從定義上看驮宴,一段本該工作的代碼壞掉了(比如變量名敲錯(cuò)),你不能用更多的代碼再去修復(fù)它呕缭。一旦你這樣做了堵泽,你就使用錯(cuò)誤處理的代碼代替了出錯(cuò)的代碼。
有些人贊成從程序員的失誤中恢復(fù)恢总,也就是讓當(dāng)前的操作失敗迎罗,但是繼續(xù)處理請求。這種做法不推薦片仿∥瓢玻考慮這樣的情況:原始代碼里有一個(gè)失誤是沒考慮到某種特殊情況。你怎么確定這個(gè)問題不會(huì)影響其他請求呢砂豌?如果其它的請求共享了某個(gè)狀態(tài)(服務(wù)器厢岂,套接字,數(shù)據(jù)庫連接池等)阳距,有極大的可能其他請求會(huì)不正常塔粒。
典型的例子是 REST 服務(wù)器(比如用Restify搭的),如果有一個(gè)請求處理函數(shù)拋出了一個(gè)ReferenceError(比如筐摘,變量名打錯(cuò))卒茬。繼續(xù)運(yùn)行下去很有肯能會(huì)導(dǎo)致嚴(yán)重的 Bug映跟,而且極其難發(fā)現(xiàn)。例如:
- 一些請求間共享的狀態(tài)可能會(huì)被變成null扬虚,undefined或者其它無效值,結(jié)果就是下一個(gè)請求也失敗了球恤。
- 數(shù)據(jù)庫(或其它)連接可能會(huì)被泄露辜昵,降低了能夠并行處理的請求數(shù)量。最后只剩下幾個(gè)可用連接會(huì)很壞咽斧,將導(dǎo)致請求由并行變成串行被處理堪置。
- 更糟的是, postgres 連接會(huì)被留在打開的請求事務(wù)里张惹。這會(huì)導(dǎo)致 postgres “持有”表中某一行的舊值舀锨,因?yàn)樗鼘@個(gè)事務(wù)可見。這個(gè)問題會(huì)存在好幾周宛逗,造成表無限制的增長坎匿,后續(xù)的請求全都被拖慢了,從幾毫秒到幾分鐘[腳注4]雷激。雖然這個(gè)問題和 postgres 緊密相關(guān)替蔬,但是它很好的說明了程序員一個(gè)簡單的失誤會(huì)讓應(yīng)用程序陷入一種非常可怕的狀態(tài)屎暇。
- 連接會(huì)停留在已認(rèn)證的狀態(tài)承桥,并且被后續(xù)的連接使用。結(jié)果就是在請求里搞錯(cuò)了用戶根悼。
- 套接字會(huì)一直打開著凶异。一般情況下NodeJS 會(huì)在一個(gè)空閑的套接字上應(yīng)用兩分鐘的超時(shí),但這個(gè)值可以覆蓋挤巡,這將會(huì)泄露一個(gè)文件描述符剩彬。如果這種情況不斷發(fā)生,程序會(huì)因?yàn)橛霉饬怂械奈募枋龇鴱?qiáng)退玄柏。即使不覆蓋這個(gè)超時(shí)時(shí)間襟衰,客戶端會(huì)掛兩分鐘直到 “hang-up” 錯(cuò)誤的發(fā)生。這兩分鐘的延遲會(huì)讓問題難于處理和調(diào)試粪摘。
- 很多內(nèi)存引用會(huì)被遺留瀑晒。這會(huì)導(dǎo)致泄露,進(jìn)而導(dǎo)致內(nèi)存耗盡徘意,GC 需要的時(shí)間增加苔悦,最后性能急劇下降。這點(diǎn)非常難調(diào)試椎咧,而且很需要技巧與導(dǎo)致造成泄露的失誤聯(lián)系起來玖详。
最好的從失誤恢復(fù)的方法是立刻崩潰把介。你應(yīng)該用一個(gè)restarter 來啟動(dòng)你的程序,在奔潰的時(shí)候自動(dòng)重啟蟋座。如果restarter 準(zhǔn)備就緒拗踢,崩潰是失誤來臨時(shí)最快的恢復(fù)可靠服務(wù)的方法。
奔潰應(yīng)用程序唯一的負(fù)面影響是相連的客戶端臨時(shí)被擾亂向臀,但是記壮彩:
- 從定義上看,這些錯(cuò)誤屬于 Bug券膀。我們并不是在討論正常的系統(tǒng)或是網(wǎng)絡(luò)錯(cuò)誤君纫,而是程序里實(shí)際存在的Bug。它們應(yīng)該在線上很罕見芹彬,并且是調(diào)試和修復(fù)的最高優(yōu)先級蓄髓。
- 上面討論的種種情形里,請求沒有必要一定得成功完成舒帮。請求可能成功完成会喝,可能讓服務(wù)器再次崩潰,可能以某種明顯的方式不正確的完成玩郊,或者以一種很難調(diào)試的方式錯(cuò)誤的結(jié)束了好乐。
- 在一個(gè)完備的分布式系統(tǒng)里,客戶端必須能夠通過重連和重試來處理服務(wù)端的錯(cuò)誤瓦宜。不管 NodeJS 應(yīng)用程序是否被允許崩潰蔚万,網(wǎng)絡(luò)和系統(tǒng)的失敗已經(jīng)是一個(gè)事實(shí)了。
- 如果你的線上代碼如此頻繁地崩潰讓連接斷開變成了問題临庇,那么正真的問題是你的服務(wù)器 Bug 太多了反璃,而不是因?yàn)槟氵x擇出錯(cuò)就崩潰。
如果出現(xiàn)服務(wù)器經(jīng)常崩潰導(dǎo)致客戶端頻繁掉線的問題假夺,你應(yīng)該把經(jīng)歷集中在造成服務(wù)器崩潰的 Bug 上淮蜈,把它們變成可捕獲的異常,而不是在代碼明顯有問題的情況下盡可能地避免崩潰已卷。調(diào)試這類問題最好的方法是梧田,把 NodeJS 配置成出現(xiàn)未捕獲異常時(shí)把內(nèi)核文件打印出來。在 GNU/Linux 或者 基于 illumos 的系統(tǒng)上使用這些內(nèi)核文件侧蘸,你不僅查看應(yīng)用崩潰時(shí)的堆棧記錄裁眯,還可以看到傳遞給函數(shù)的參數(shù)和其它的 JavaScript 對象,甚至是那些在閉包里引用的變量讳癌。即使沒有配置 code dumps穿稳,你也可以用堆棧信息和日志來開始處理問題。
最后晌坤,記住程序員在服務(wù)器端的失誤會(huì)造成客戶端的操作失敗逢艘,還有客戶端必須處理好服務(wù)器端的奔潰和網(wǎng)絡(luò)中斷旦袋。這不只是理論,而是實(shí)際發(fā)生在線上環(huán)境里它改。
編寫函數(shù)的實(shí)踐
我們已經(jīng)討論了如何處理異常疤孕,那么當(dāng)你在編寫新的函數(shù)的時(shí)候,怎么才能向調(diào)用者傳遞錯(cuò)誤呢央拖?
最最重要的一點(diǎn)是為你的函數(shù)寫好文檔胰柑,包括它接受的參數(shù)(附上類型和其它約束),返回值爬泥,可能發(fā)生的錯(cuò)誤,以及這些錯(cuò)誤意味著什么崩瓤。如果你不知道會(huì)導(dǎo)致什么錯(cuò)誤或者不了解錯(cuò)誤的含義袍啡,那你的應(yīng)用程序正常工作就是一個(gè)巧合。所以却桶,當(dāng)你編寫新的函數(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)用者都沒有用趁啸,那么程序通常情況下會(huì)崩潰(異常也可能會(huì)被domains或者進(jìn)程級的uncaughtException捕捉到强缘,詳見下文)。
- Callback是最基礎(chǔ)的異步傳遞事件的一種方式不傅。用戶傳進(jìn)來一個(gè)函數(shù)(callback)旅掂,之后當(dāng)某個(gè)異步操作完成后調(diào)用這個(gè) callback。通常 callback 會(huì)以callback(err,result)的形式被調(diào)用访娶,這種情況下商虐, err和 result必然有一個(gè)是非空的,取決于操作是成功還是失敗崖疤。
- 更復(fù)雜的情形是秘车,函數(shù)沒有用 Callback 而是返回一個(gè) EventEmitter 對象,調(diào)用者需要監(jiān)聽這個(gè)對象的 error事件劫哼。這種方式在兩種情況下很有用鲫尊。
- 當(dāng)你在做一個(gè)可能會(huì)產(chǎn)生多個(gè)錯(cuò)誤或多個(gè)結(jié)果的復(fù)雜操作的時(shí)候。比如沦偎,有一個(gè)請求一邊從數(shù)據(jù)庫取數(shù)據(jù)一邊把數(shù)據(jù)發(fā)送回客戶端疫向,而不是等待所有的結(jié)果一起到達(dá)咳蔚。在這個(gè)例子里,沒有用 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ī)的對象上,這些對象往往伴隨著大量的異步事件囊嘉。例如温技,一個(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ù)本身是同步的還是異步的?
直到目前浪汪,最常見的例子是在異步函數(shù)里發(fā)生了操作失敗巴柿。在大多數(shù)情況下,你需要寫一個(gè)以回調(diào)函數(shù)作為參數(shù)的函數(shù)死遭,然后你會(huì)把異常傳遞給這個(gè)回調(diào)函數(shù)广恢。這種方式工作的很好,并且被廣泛使用呀潭。例子可參照 NodeJS 的 fs 模塊钉迷。如果你的場景比上面這個(gè)還復(fù)雜至非,那么你可能就得換用 EventEmitter 了,不過你也還是在用異步方式傳遞這個(gè)錯(cuò)誤糠聪。
其次常見的一個(gè)例子是像JSON.parse 這樣的函數(shù)同步產(chǎn)生了一個(gè)異常荒椭。對這些函數(shù)而言,如果遇到操作失斀Ⅲ (比如無效輸入)趣惠,你得用同步的方式傳遞它。你可以拋出(更加常見)或者返回它身害。
對于給定的函數(shù)味悄,如果有一個(gè)異步傳遞的異常,那么所有的異常都應(yīng)該被異步傳遞塌鸯∈躺可能有這樣的情況,請求一到來你就知道它會(huì)失敗界赔,并且知道不是因?yàn)槌绦騿T的失誤∏4ィ可能的情形是你緩存了返回給最近請求的錯(cuò)誤淮悼。雖然你知道請求一定失敗,但是你還是應(yīng)該用異步的方式傳遞它揽思。
通用的準(zhǔn)則就是你即可以同步傳遞錯(cuò)誤(拋出)袜腥,也可以異步傳遞錯(cuò)誤(通過傳給一個(gè)回調(diào)函數(shù)或者觸發(fā) EventEmitter 的 error事件),但是不用同時(shí)使用钉汗。以這種方式羹令,用戶處理異常的時(shí)候可以選擇用回調(diào)函數(shù)還是用try/catch,但是不需要兩種都用损痰。具體用哪一個(gè)取決于異常是怎么傳遞的福侈,這點(diǎn)得在文檔里說明清楚。
差點(diǎn)忘了程序員的失誤卢未》玖荩回憶一下,它們其實(shí)是 Bug 辽社。在函數(shù)開頭通過檢查參數(shù)的類型(或是其它約束)就可以被立即發(fā)現(xiàn)伟墙。一個(gè)退化的例子是,某人調(diào)用了一個(gè)異步的函數(shù)滴铅,但是沒有傳回調(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)則并沒有因?yàn)檫@條意見而改變。如果你想知道更多邻梆,請見上面的 (不要)處理程序員的失誤守伸。
下表以 NodeJS 核心模塊的常見函數(shù)為例,做了一個(gè)總結(jié)浦妄,大致按照每種問題出現(xiàn)的頻率來排列:
異步函數(shù)里出現(xiàn)操作錯(cuò)誤的例子(第一行)是最常見的尼摹。在同步函數(shù)里發(fā)生操作失敗(第二行)比較少見剂娄,除非是驗(yàn)證用戶輸入蠢涝。程序員失誤(第三行)除非是在開發(fā)環(huán)境下,否則永遠(yuǎn)都不應(yīng)該出現(xiàn)阅懦。
吐槽:程序員失誤還是操作失敽投?
你怎么知道是程序員的失誤還是操作失敗呢耳胎?很簡單惯吕,你自己來定義并且記在文檔里,包括允許什么類型的函數(shù)怕午,怎樣打斷它的執(zhí)行废登。如果你得到的異常不是文檔里能接受的,那就是一個(gè)程序員失誤郁惜。如果在文檔里寫明接受但是暫時(shí)處理不了的堡距,那就是一個(gè)操作失敗。
你得用你的判斷力去決定你想做到多嚴(yán)格兆蕉,但是我們會(huì)給你一定的意見羽戒。具體一些,想象有個(gè)函數(shù)叫做“connect”虎韵,它接受一個(gè)IP地址和一個(gè)回調(diào)函數(shù)作為參數(shù)易稠,這個(gè)回調(diào)函數(shù)會(huì)在成功或者失敗的時(shí)候被調(diào)用。現(xiàn)在假設(shè)用戶傳進(jìn)來一個(gè)明顯不是IP地址的參數(shù)包蓝,比如“bob”缩多,這個(gè)時(shí)候你有幾種選擇:
- 在文檔里寫清楚只接受有效的IPV4的地址,當(dāng)用戶傳進(jìn)來“bob”的時(shí)候拋出一個(gè)異常养晋。強(qiáng)烈推薦這種做法衬吆。
- 在文檔里寫上接受任何string類型的參數(shù)。如果用戶傳的是“bob”绳泉,觸發(fā)一個(gè)異步錯(cuò)誤指明無法連接到“bob”這個(gè)IP地址逊抡。
這兩種方式和我們上面提到的關(guān)于操作失敗和程序員失誤的指導(dǎo)原則是一致的。你決定了這樣的輸入算是程序員的失誤還是操作失敗。通常冒嫡,用戶輸入的校驗(yàn)是很松的拇勃,為了證明這點(diǎn),可以看Date.parse這個(gè)例子孝凌,它接受很多類型的輸入方咆。但是對于大多數(shù)其它函數(shù),我們強(qiáng)烈建議你偏向更嚴(yán)格而不是更松蟀架。你的程序越是猜測用戶的本意(使用隱式的轉(zhuǎn)換瓣赂,無論是JavaScript語言本身這么做還是有意為之),就越是容易猜錯(cuò)片拍。本意是想讓開發(fā)者在使用的時(shí)候不用更加具體煌集,結(jié)果卻耗費(fèi)了人家好幾個(gè)小時(shí)在Debug上。再說了捌省,如果你覺得這是個(gè)好主意苫纤,你也可以在未來的版本里讓函數(shù)不那么嚴(yán)格,但是如果你發(fā)現(xiàn)由于猜測用戶的意圖導(dǎo)致了很多惱人的bug纲缓,要修復(fù)它的時(shí)候想保持兼容性就不大可能了卷拘。
所以如果一個(gè)值怎么都不可能是有效的(本該是string卻得到一個(gè)undefined,本該是string類型的IP但明顯不是)祝高,你應(yīng)該在文檔里寫明是這不允許的并且立刻拋出一個(gè)異常栗弟。只要你在文檔里寫的清清楚楚,那這就是一個(gè)程序員的失誤而不是操作失敗褂策。立即拋出可以把Bug帶來的損失降到最小横腿,并且保存了開發(fā)者可以用來調(diào)試這個(gè)問題的信息(例如颓屑,調(diào)用堆棧斤寂,如果用內(nèi)核文件還可以得到參數(shù)和內(nèi)存分布)。
那么 domains 和 process.on('uncaughtException') 呢揪惦?
操作失敗總是可以被顯示的機(jī)制所處理的:捕獲一個(gè)異常遍搞,在回調(diào)里處理錯(cuò)誤,或者處理 EventEmitter 的“error”事件等等器腋。Domains以及進(jìn)程級別的‘uncaughtException’主要是用來從未料到的程序錯(cuò)誤恢復(fù)的溪猿。由于上面我們所討論的原因,這兩種方式都不鼓勵(lì)纫塌。
編寫新函數(shù)的具體建議
我們已經(jīng)談?wù)摿撕芏嘀笇?dǎo)原則诊县,現(xiàn)在讓我們具體一些。
你的函數(shù)做什么得很清楚措左。 這點(diǎn)非常重要依痊。每個(gè)接口函數(shù)的文檔都要很清晰的說明: - 預(yù)期參數(shù) - 參數(shù)的類型 - 參數(shù)的額外約束(例如,必須是有效的IP地址) 如果其中有一點(diǎn)不正確或者缺少怎披,那就是一個(gè)程序員的失誤胸嘁,你應(yīng)該立刻拋出來瓶摆。 此外,你還要記錄:
- 調(diào)用者可能會(huì)遇到的操作失斝院辍(以及它們的 name)
- 怎么處理操作失斎壕(例如是拋出,傳給回調(diào)函數(shù)毫胜,還是被 EventEmitter 發(fā)出)
- 返回值
- 使用 Error 對象或它的子類书斜,并且實(shí)現(xiàn) Error 的協(xié)議。
你的所有錯(cuò)誤要么使用Error 類要么使用它的子類指蚁。你應(yīng)該提供name和message屬性菩佑,stack也是(注意準(zhǔn)確)。 - 在程序里通過 Error 的 name屬性區(qū)分不同的錯(cuò)誤凝化。
當(dāng)你想要知道錯(cuò)誤是何種類型的時(shí)候稍坯,用 name 屬性。 JavaScript 內(nèi)置的供你重用的名字包括“RangeError”(參數(shù)超出有效范圍)和“TypeError”(參數(shù)類型錯(cuò)誤)搓劫。而 HTTP 異常瞧哟,通常會(huì)用 RFC 指定的名字,比如“BadRequestError”或者“ServiceUnavailableError”枪向。 - 不要想著給每個(gè)東西都取一個(gè)新的名字勤揩。
如果你可以只用一個(gè)簡單的 InvalidArgumentError,就不要分成 InvalidHostnameError秘蛔,InvalidIpAddressError陨亡,InvalidDnsError等等,你要做的是通過增加屬性來說明那里出了問題(下面會(huì)講到)深员。 - 用詳細(xì)的屬性來增強(qiáng) Error 對象负蠕。
舉個(gè)例子,如果遇到無效參數(shù)倦畅,把 propertyName 設(shè)成參數(shù)的名字遮糖,把 propertyValue 設(shè)成傳進(jìn)來的值。如果無法連到服務(wù)器叠赐,用 remoteIp 屬性指明嘗試連接到的 IP欲账。如果發(fā)生一個(gè)系統(tǒng)錯(cuò)誤,在syscal 屬性里設(shè)置是哪個(gè)系統(tǒng)調(diào)用芭概,并把錯(cuò)誤代碼放到 errno屬性里赛不。具體你可以查看附錄,看有哪些樣例屬性可以用罢洲。
至少需要這些屬性:
name:用于在程序里區(qū)分眾多的錯(cuò)誤類型(例如參數(shù)非法和連接失斕吖省)
message:一個(gè)供人類閱讀的錯(cuò)誤消息。對可能讀到這條消息的人來說這應(yīng)該已經(jīng)足夠完整。如果你從更底層的地方傳遞了一個(gè)錯(cuò)誤畴椰,你應(yīng)該加上一些信息來說明你在做什么抓艳。怎么包裝異常請往下看玷或。
stack:一般來講不要隨意擾亂堆棧信息。甚至不要增強(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)頁上的一個(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ò)誤尸执。(請注意缓醋,第二部分并不總是跟在第一部分之后。有的時(shí)候funcA會(huì)重新嘗試送粱。有的時(shí)候又希望funcA忽略錯(cuò)誤因?yàn)闊o事可做。但在這里抗俄,我們只討論funcA直接返回funcB錯(cuò)誤的情況)
在這個(gè)例子里,可以考慮包裝這個(gè)錯(cuò)誤而不是直接返回它动雹。包裝的意思是繼續(xù)拋出一個(gè)包含底層信息的新的異常槽卫,并且?guī)袭?dāng)前層的上下文胰蝠。用 verror 這個(gè)包可以很簡單的做到這點(diǎn)。
舉個(gè)例子噪窘,假設(shè)有一個(gè)函數(shù)叫做 fetchConfig梗搅,這個(gè)函數(shù)會(huì)到一個(gè)遠(yuǎn)程的數(shù)據(jù)庫取得服務(wù)器的配置。你可能會(huì)在服務(wù)器啟動(dòng)的時(shí)候調(diào)用這個(gè)函數(shù)丐枉。整個(gè)流程看起來是這樣的:
1.加載配置 1.1 連接數(shù)據(jù)庫 1.1.1 解析數(shù)據(jù)庫服務(wù)器的DNS主機(jī)名 1.1.2 建立一個(gè)到數(shù)據(jù)庫服務(wù)器的TCP連接 1.1.3 向數(shù)據(jù)庫服務(wù)器認(rèn)證 1.2 發(fā)送DB請求 1.3 解析返回結(jié)果 1.4 加載配置 2 開始處理請求
假設(shè)在運(yùn)行時(shí)出了一個(gè)問題連接不到數(shù)據(jù)庫服務(wù)器哆键。如果連接在 1.1.2 的時(shí)候因?yàn)闆]有到主機(jī)的路由而失敗了,每個(gè)層都不加處理地都把異常向上拋出給調(diào)用者瘦锹。你可能會(huì)看到這樣的異常信息:
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辱士。
你可能會(huì)想跳過其中幾層的封裝來得到一條不那么充滿學(xué)究氣息的消息:
myserver: failed to load configuration: connection refused from database at 127.0.0.1 port 1234.
不過話又說回來,報(bào)錯(cuò)的時(shí)候詳細(xì)一點(diǎn)總比信息不夠要好听绳。
如果你決定封裝一個(gè)異常了颂碘,有幾件事情要考慮:
- 保持原有的異常完整不變,保證當(dāng)調(diào)用者想要直接用的時(shí)候底層的異常還可用椅挣。
- 要么用原有的名字头岔,要么顯示地選擇一個(gè)更有意義的名字。例如鼠证,最底層是 NodeJS 報(bào)的一個(gè)簡單的Error峡竣,但在步驟1中可以是個(gè) IntializationError 。(但是如果程序可以通過其它的屬性區(qū)分量九,不要覺得有責(zé)任取一個(gè)新的名字)
- 要么用原有的名字适掰,要么顯示地選擇一個(gè)更有意義的名字。例如荠列,最底層是 NodeJS 報(bào)的一個(gè)簡單的Error类浪,但在步驟1中可以是個(gè) IntializationError 。(但是如果程序可以通過其它的屬性區(qū)分弯予,不要覺得有責(zé)任取一個(gè)新的名字)
在 Joyent戚宦,我們使用verror 這個(gè)模塊來封裝錯(cuò)誤个曙,因?yàn)樗恼Z法簡潔锈嫩。寫這篇文章的時(shí)候受楼,它還不能支持上面的所有功能,但是會(huì)被擴(kuò)展以期支持呼寸。
例子
考慮有這樣的一個(gè)函數(shù)艳汽,這個(gè)函數(shù)會(huì)異步地連接到一個(gè) IPv4 地址的 TCP 端口。我們通過例子來看文檔怎么寫:
/*
* 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è)例子在概念上很簡單对雪,但是展示了上面我們所談?wù)摰囊恍┙ㄗh:
- 參數(shù)河狐,類型以及其它一些約束被清晰的文檔化。
- 這個(gè)函數(shù)對于接受的參數(shù)是非常嚴(yán)格的瑟捣,并且會(huì)在得到錯(cuò)誤參數(shù)的時(shí)候拋出異常(程序員的失誤)馋艺。
- 可能出現(xiàn)的操作失敗集合被記錄了。通過不同的”name“值可以區(qū)分不同的異常迈套,而”errno“被用來獲得系統(tǒng)錯(cuò)誤的詳細(xì)信息捐祠。
- 異常被傳遞的方式也被記錄了(通過失敗時(shí)調(diào)用回調(diào)函數(shù))。
- 返回的錯(cuò)誤有”remoteIp“和”remotePort“字段桑李,這樣用戶就可以定義自己的錯(cuò)誤了(比如踱蛀,一個(gè)HTTP客戶端的端口號是隱含的)。
- 雖然很明顯贵白,但是連接失敗后的狀態(tài)也被清晰的記錄了:所有被打開的套接字此時(shí)已經(jīng)被關(guān)閉率拒。
這看起來像是給一個(gè)很容易理解的函數(shù)寫了超過大部分人會(huì)寫的的超長注釋,但大部分函數(shù)實(shí)際上沒有這么容易理解禁荒。所有建議都應(yīng)該被有選擇的吸收猬膨,如果事情很簡單,你應(yīng)該自己做出判斷呛伴,但是記琢绕:用十分鐘把預(yù)計(jì)發(fā)生的記錄下來可能之后會(huì)為你或其他人節(jié)省數(shù)個(gè)小時(shí)。
總結(jié)
- 學(xué)習(xí)了怎么區(qū)分操作失敗磷蜀,即那些可以被預(yù)測的哪怕在正確的程序里也無法避免的錯(cuò)誤(例如召耘,無法連接到服務(wù)器);而程序的Bug則是程序員失誤褐隆。
- 操作失敗可以被處理,也應(yīng)當(dāng)被處理污它。程序員的失誤無法被處理或可靠地恢復(fù)(本不應(yīng)該這么做),嘗試這么做只會(huì)讓問題更難調(diào)試庶弃。
- 一個(gè)給定的函數(shù)衫贬,它處理異常的方式要么是同步(用 throw方式)要么是異步的(用 callback 或者 EventEmitter),不會(huì)兩者兼具歇攻。用戶可以在回調(diào)函數(shù)里處理錯(cuò)誤固惯,也可以使用 try/catch捕獲異常 ,但是不能一起用缴守。實(shí)際上葬毫,使用throw并且期望調(diào)用者使用 try/catch 是很罕見的镇辉,因?yàn)?NodeJS里的同步函數(shù)通常不會(huì)產(chǎn)生運(yùn)行失敗(主要的例外是類似于JSON.parse的用戶輸入驗(yàn)證函數(shù))贴捡。
- 在寫新函數(shù)的時(shí)候忽肛,用文檔清楚地記錄函數(shù)預(yù)期的參數(shù),包括它們的類型烂斋、是否有其它約束(例如必須是有效的 IP 地址)屹逛,可能會(huì)發(fā)生的合理的操作失敗(例如無法解析主機(jī)名汛骂,連接服務(wù)器失敗罕模,所有的服務(wù)器端錯(cuò)誤),錯(cuò)誤是怎么傳遞給調(diào)用者的(同步帘瞭,用throw手销,還是異步,用 callback 和 EventEmitter)图张。
- 缺少參數(shù)或者參數(shù)無效是程序員的失誤锋拖,一旦發(fā)生總是應(yīng)該拋出異常。函數(shù)的作者認(rèn)為的可接受的參數(shù)可能會(huì)有一個(gè)灰色地帶祸轮,但是如果傳遞的是一個(gè)文檔里寫明接收的參數(shù)以外的東西兽埃,那就是一個(gè)程序員失誤。
- 傳遞錯(cuò)誤的時(shí)候用標(biāo)準(zhǔn)的 Error 類和它標(biāo)準(zhǔn)的屬性适袜。盡可能把額外的有用信息放在對應(yīng)的屬性里柄错。如果有可能,用約定的屬性名(如下)苦酱。
附錄:Error 對象屬性命名約定
強(qiáng)烈建議你在發(fā)生錯(cuò)誤的時(shí)候用這些名字來保持和 Node 核心以及 Node 插件的一致售貌。這些大部分不會(huì)和某個(gè)給定的異常對應(yīng),但是出現(xiàn)疑問的時(shí)候疫萤,你應(yīng)該包含任何看起來有用的信息颂跨,即從編程上也從自定義的錯(cuò)誤消息上。
腳注
- 人們有的時(shí)候會(huì)這么寫代碼扯饶,他們想要在出現(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)用的兰怠,外面并沒有 try 的代碼塊梦鉴。如果你用這個(gè)反模式李茫,結(jié)果就是拋出異常的時(shí)候,程序崩潰了尚揣。
- 在 JavaScript 里涌矢,拋出一個(gè)不屬于Error的參數(shù)從技術(shù)上是可行的掖举,但是應(yīng)該被避免快骗。這樣的結(jié)果使獲得調(diào)用堆棧沒有可能,代碼也無法檢查name屬性塔次,或者其它任何能夠說明哪里有問題的屬性方篮。
- 操作失敗和程序員的失誤這一概念早在 NodeJS 之前就已經(jīng)存在存在了。不嚴(yán)格地對應(yīng)者 Java 里的 checked 和 unchecked 異常励负,雖然操作失敗被認(rèn)為是無法避免的藕溅,比如 OutOfMemeoryError,被歸為 uncheked 異常继榆。在 C 語言里有對應(yīng)的概念巾表,普通異常處理和使用斷言。維基百科上關(guān)于斷言的的文章也有關(guān)于什么時(shí)候用斷言什么時(shí)候用普通的錯(cuò)誤處理的類似的解釋略吨。
- 如果這看起來非常具體集币,那是因?yàn)槲覀冊诋a(chǎn)品環(huán)境中遇到這樣過這樣的問題。這真的很可怕翠忠。