作者:vienwu
本文内容大部分來自 https://www.joyent.com/node-js/production/design/errors ,原文比較長,感覺也有點啰嗦,是以根據個人了解猜測梳理出本文,如果有錯誤歡迎指出,謝謝!
很多人其實不是很重視錯誤處理,但對于建構一個健壯的nodejs應用,錯誤處理是非常重要的一件事情,希望本文可以給你一些啟發。
先抛出幾個問題:
應該用哪種方式暴露錯誤?<code>throw</code>、<code>callback(err, result)</code>、<code>Event Emitter</code>或者其他方式?
如何假設函數的參數?是否應該檢測類型正确?非null,IP,QQ号碼?
函數參數不符合預期該怎麼處理?
應該如何區分不同類型的錯誤?例如<code>Bad Request</code>、 <code>Service Unavailable</code>。
應該如何提供有用的錯誤資訊?
應該如何捕獲錯誤?使用<code>try/catch</code>,還是<code>domains</code>或者其他方式?
關于<code>Error</code>、<code>throw</code>、<code>try...catch</code>的一些基礎知識連結
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch
node.js v7.2.0 <code>domain</code>、<code>process</code>
https://nodejs.org/api/domain.html
https://nodejs.org/api/process.html
verror子產品: rich JavaScript errors
https://github.com/joyent/node-verror
抛出錯誤的幾種方式:
捕獲錯誤
一般來說,我們将錯誤簡單的分為兩種類型:操作錯誤、編碼錯誤。
對于有經驗的人來說,寫代碼的時候都會處理一些常見的操作錯誤,例如<code>JSON.parse</code>總是會和<code>try...catch</code>一起,例如網絡故障、遠端伺服器傳回500等。這些錯誤并非bug。
對于程式來說,另外一種錯誤屬于編碼錯誤,這是程式的bug,解決的方式應該是修改代碼,避免發生。例如<code>read property of "undefined"</code>、調用一個異步函數但沒有傳入callback、函數參數預期是<code>Object</code>但是傳了一個<code>String</code>等等。
人們在談論錯誤時,總是将這兩種錯誤混在一起,實際上這兩種錯誤是完全不同的。例如<code>File not found</code>是一種操作錯誤,但這不能說明哪裡出錯了,這可能僅僅表示程式應該先建立檔案。
有些時候,同一個問題可能會導緻多種錯誤。例如nodejs應用因為一個變量undefined導緻crash,這是編碼錯誤,用戶端則會接收到<code>ECONNRESET</code>錯誤,這屬于操作錯誤,對于用戶端來說應該可以預期到伺服器的這個錯誤。
對于明确的操作錯誤類型,直接處理掉。
例如嘗試打開一個log檔案可能會導緻 ENOENT ,那麼建立這個檔案即可。
對于預料之外你不知道如何處理的錯誤,比較好的方式是記錄error并crash,傳遞合适的錯誤資訊給用戶端。
最好的方式是立即crash。
這種錯誤是程式的bug,一般來說寫再多的代碼也避免不了。因為在node應用中,我們一般會監控挂掉的程序并自動重新開機,是以立即crash是比較好的方式。
調試這類問題的最佳方式,是在捕獲到<code>uncaught exception</code>的時候,記錄相關資訊。
總之記住,server的代碼錯誤(bug)傳遞到client時會成為一個操作錯誤,例如server捕獲到<code>uncaught exception</code>則傳回一個500,用戶端來處理這個操作錯誤。
首先,最重要的是文檔,描述這個函數做了些什麼,接收什麼類型的參數傳回什麼,可能會觸發什麼錯誤。
一些基本原則:
同步的函數裡,使用<code>throw</code>。使用者使用<code>try...catch</code>即可捕獲錯誤。
異步函數裡,更常用的方式是使用<code>callback(err, result)</code>的方式。
在更複雜的場景裡,可以傳回一個<code>EventEmitter</code>對象,代替使用<code>callback</code>。使用者可以監聽<code>emitter</code>對象的 <code>error</code>事件。 例如讀取一個資料流,我們可能會同時使用 <code>req.on('data')</code>、<code>req.on('error')</code>、<code>req.on('timeout')</code>。
是以,使用<code>throw</code>還是<code>callbacks</code>、<code>EventEmitter</code>,取決于:
該錯誤是操作錯誤還是編碼錯誤?
該函數是同步還是異步?
此外,不管是同步(使用throw)或者異步(使用<code>callback</code>或<code>EventEmitter</code>),隻使用一種方式傳遞錯誤,避免同時使用兩種方式。這樣的話,使用者就隻需要使用一種方式來捕獲錯誤,例如<code>try...catch</code>或者<code>callback</code>,不需要考慮更多的場景。
下面用一個特例來說明這一點:
在上例的第二種情況,會立即傳回<code>TypeError: path must be a string or Buffer</code>,也就是說内部使用了<code>throw</code>,這種情況是不是和上面提到的有沖突?
其實并不是,第二種情況屬于編碼錯誤(<code>fs.stat</code>隻接收路徑作為參數但我們給了他一個<code>null</code>),并不是操作錯誤。編碼錯誤永遠不應該被處理。
是以在使用<code>fs.stat</code>的時,使用者仍然隻需要處理<code>callback</code>傳遞的錯誤,不需要使用<code>try...catch</code>。
這一點取決于函數申明的可以允許的類型,以及你如何來解釋它們:
如果得到的參數和申明的類型(不一定是指資料類型,也可能是IP位址、QQ号等類型)不一緻,那麼屬于編碼錯誤(使用者應該使用符合要求的參數)
如果得到的參數和申明的類型一緻,但函數不能處理這種情況,那屬于操作錯誤。
你必須決定限制類型的嚴格程度。
例如需要連接配接到一個伺服器,函數接收一個ip位址作為參數,那麼有幾種做法:
函數隻接收ip位址格式的參數,如果不符合格式,則立即抛出異常。
函數接收任意字元串參數,如果參數不是ip位址格式,則使用callback發出一個異步錯誤,提示無法連接配接該位址。
這兩種做法決定了同樣的輸入會導緻編碼錯誤或操作錯誤。對于大多數功能,我們強烈建議更嚴格,因為更寬松的限制會更容易導緻使用錯誤以及浪費時間。
操作錯誤一般都可以使用明确的機制來處理(根據具體的錯誤對應處理,使用<code>try...catch</code>、<code>callback</code>、<code>EventEmitter</code>等)。
domain和全局的異常捕獲主要是為了發現和處理未預料到的編碼錯誤。
清楚function的功能
必須明确幾點:期待的參數、參數類型、額外限制(IP位址、QQ号碼等)。
如果任意一點不比對,則立即抛出<code>throw</code>異常。
此外,還應該有:
使用方可以預料到的操作錯誤、如何捕獲這些錯誤、傳回值。
所有的erorr都使用Error對象(或者基于Error類的擴充)
所有的error都應該提供<code>name</code>和<code>message</code>屬性,并且<code>stack</code>也應該準确可用。
使用<code>name</code>屬性來區分錯誤類型
例如<code>RangeError</code>、<code>TypeError</code>。
不要為每種錯誤取個名字,例如定義<code>InvalidHostnameError</code>、<code>InvalidIpAddressError</code>這種來表示具體的錯誤,對于這種錯誤可以統一用<code>InvalidArgumentError</code>表示錯誤類型,然後在較長的描述裡補充更多資訊。
增加解釋錯誤細節的屬性
例如無法連接配接到伺服器,可以增加一個<code>remoteIp</code> 屬性表示試圖連接配接的ip。
如果傳遞一個較低級别的錯誤,考慮重新包裝錯誤。
如果函數調用順序如下:funcA -> funcB -> funcC,funcC傳回一個加載配置失敗的錯誤,funcB連接配接伺服器失敗。
那麼,在funcA中,更希望得到包含這2個錯誤的資訊。是以在funcB中捕獲到funcC的錯誤時,包裝并傳遞這些錯誤是有價值的。
包裝底層的錯誤資訊時,盡可能保留原始的資訊,除了名稱<code>name</code>,但不要改寫原始的error對象。
一個組合多個錯誤的示例:
這裡有一個庫可以幫我們做這件事:
區分錯誤類型,是可預見的還是不可避免的,是操作錯誤還是bug。
操作錯誤應該被處理。編碼錯誤不應該被處理(全局處理并記錄)。
一個函數可能産生的操作錯誤,隻應該使用同步(<code>throw</code>)或者異步一種方式。一般來說,在nodejs中,同步函數導緻的操作錯誤是比較少見的,使用<code>try...catch</code>會很少,常見的是使用者輸入驗證如JSON、解析等。
一個函數的參數、類型、預期錯誤、如何捕獲都應該是明确的。
缺少參數、參數無效都屬于編碼錯誤,應該直接抛出異常(<code>throw</code>)。
使用标準的Error類和标準屬性。使用獨立的屬性,添加盡可能多的附加資訊,盡可能使用通用的屬性名稱。
例如一些常見的屬性名稱:
<code>localHostname、localIp、localPort、remoteHostname、remoteIp、remotePort、path、srcpath、dstpath、hostname、ip、propertyName、propertyValue、syscall、errno</code>
不要嘗試用<code>try...catch</code>去捕獲一個異步函數的錯誤,這樣會什麼也得不到。
如果不是産生錯誤,不要使用<code>throw</code>。
nodejs之前就已經有操作錯誤、編碼錯誤的概念,參考這裡 https://en.wikipedia.org/wiki/Assertion_%28software_development%29#Comparison_with_error_handling
原文連結:http://ivweb.io/topic/5846d1d4270eedfd10a0f5eb