天天看點

nodejs 中錯誤捕獲的一些最佳實踐

作者: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 -&gt; funcB -&gt; 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