在錯誤處理方面,JavaScript一開始只有try/catch/finally,以及幾個內建的錯誤類型,簡單到令人訝異,隨著應用層面增多與規範的發展,在錯誤處理這方面,應是值得重視的一環。

有限的標準錯誤類型

雖然JavaScript有try/catch/finally/throw等錯誤處理相關語法,也有Error等錯誤類型,然而,相對於其他程式語言的錯誤處理機制來說,似乎過於精簡,原因之一在於,JavaScript最初執行於瀏覽器,可用的資源相對有限,若有錯誤發生,通常就是難以處理或不該處理的情況,內建標準錯誤類型都有個Error字眼(而不是Exception),也反映這個事實。

另一方面,只能有一個catch區塊,由於catch的語法本身無法區別型態,若要判定錯誤類型,必須自行使用if結合instanceof判斷,ES10甚至還有個〈Optional catch binding〉(https://bit.ly/2McB9ui),若catch後不需要錯誤實例,可以不用直接寫catch就好了,不需要括號與參考錯誤實例的名稱。

在類型的數量上,通用的Error加上其子類型,總共才7個錯誤類型(其中SyntaxError其實無法catch處理),但這並不是表示錯誤處理在JavaScript中不重要,而是因為ECMAScript僅規範語言本身,標準錯誤類型如SyntaxError、ReferenceError、TypeError等,多半也是跟語言本身相關的錯誤,至於不同環境或程式庫的錯誤類型,是由各個環境自行實作。

例如,在XMLHttpRequest的規範中,就有DOMException、SecurityError等例外類型,而在Node.js將JavaScript帶出瀏覽器之後,環境不再受限,但情況也變得複雜許多。例如,Node.js定義了自己的一套錯誤類型,像是AssertionError、SystemError等,基本上,Node.js是JavaScript後端的產業標準,對於Error本身,也不客氣地擴充了一些特性,這可以在Node.js官方的〈Errors〉中查詢。

自訂錯誤類型

雖然撰寫程式時若要拋出錯誤,使用ES標準本身的7個錯誤類型也可以,不過,建議還是為應用程式或程式庫自訂錯誤類型。

JavaScript的錯誤處理語法,基本上借鏡Java,而Java對例外的區分方式就像〈Error Handling in Node.js〉(https://bit.ly/2ek7IAd),將錯誤分為操作錯誤(Operational errors),以及程式設計者錯誤(Programmer errors),就有異曲同工之處。

關於程式設計者錯誤,顧名思義,導因於開發者撰寫的程式本身有臭蟲,無論是邏輯、流程或是類型上的錯誤,這類錯誤最好的處理方式就是不處理(令程式中止),並透過瀏覽器中的開發者工具,例如除錯器(Debugger)來糾出、修正這類臭蟲,要類比的話,就像是Java裡頭的執行時期錯誤。

而操作錯誤,並非程式臭蟲引發,而是來自系統環境,像是錯誤的使用者輸入、無法獲取網路連線或其他環境資源的問題,必要時,須用try/catch/finally等語法來處理。這就像Java裡頭的受檢例外,當然,JavaScript並沒有強制處理錯誤的機制,但概念是相同的。因此,根據不同狀況,可以繼承Error來自訂操作錯誤類型,以及程式設計者錯誤類型。

在自訂錯誤時,要注意Error的標準特性只有message與name──message為實例擁有,是建立實例時指定的錯誤訊息,name則是定義在原型物件上代表錯誤類型的名稱。它們的writable與configurable為true,enumerable為false,在繼承Error定義子類型時,最好遵照這些屬性的設定,透過Object.defineProperty設定,至於相關程式範例,我們可參考〈Subclassing `Error` in modern JavaScript〉。

錯誤處理時重要的訊息是堆疊追蹤,但大部份瀏覽器或Node.js僅支援非標準的stack特性。也因為非標準,建議僅在開發階段使用,倒是TC39有個階段一的〈Error Stacks〉提案,打算將stack標準化;在Node.js,我們還能用Error.captureStackTrace來處理堆疊追蹤,可參考〈Extending Error in Javascript〉。

非同步與錯誤處理

JavaScript雖然提供try/catch/finally語法來處理錯誤,然而,這些語法是同步情境,若在try區塊進行非同步操作,錯誤發生時,是不會對應的catch區塊捕捉的(因執行流程早已離開try區塊)。為處理這類問題,早期非同步操作在接受回呼函式時,回呼函式本身須有個參數接受錯誤實例,並在回呼函式檢查是否傳入錯誤實例,藉此處理相對應的錯誤。

這類回呼模式易引發回呼地獄(callback hell),ES6為此規範了Promise,在建立Promise實例時,指定的回呼中可以拋出錯誤,此時,會隱含地使用錯誤物件作為引數來駁回(reject)約定,如果想要處理例外,我們可以在Promise實例的then方法第二個參數指定函式,或者使用catch方法來處理。

在建立Promise的回呼中拋出錯誤,其實是不建議的,明確地建立錯誤實例作為引數,並呼叫駁回函式是更好的做法。如果Prmise被駁回、又沒有在then或catch中處理,最後是由不同環境實作自行決定處理方式,在瀏覽器中,我們可以透過註冊unhandledrejection事件來處理,若沒有阻止事件傳播,錯誤訊息最後會輸出至主控臺。

在Node.js中,Prmise被駁回若沒有處理,可以透過process.on來註冊unhandledRejection事件處理,若沒有處理會產生UnhandledPromiseRejectionWarning,然而,這個行為已棄用,未來會採中斷程式的作法;對於最後一定得進行的資源收尾動作,ES9為Promise增加了finally方法。

Promise雖然解決回呼地獄的問題,但回呼函式的形式,可讀性並不是很好,為此,ES8增加async語法,我們可以使用同步風格的程式碼來構築非同步流程──async函式執行完,會傳回Promise實例;函式流程中若以return結束,相當於解析(resolve),return的值會是解析時傳給解析函式的值;函式中若拋出錯誤,相當於以錯誤實例作為引數進行駁回。

如果函式傳回Promise,ES8提供了await語法與之搭配;若Promise尚未解析或駁回,執行流程會轉移,若流程又回到await而Promise已經解析,await會取得解析的結果值。因此,就程式碼的閱讀上,await就像是等待Promise的執行結果,取得結果後流程才繼續往下運行,若Promise實際上被駁回,那麼,駁回時指定的錯誤實例,會在await處拋出。

也就是說,async/await的情況下,若要處理例外,就可以使用傳統的try/catch/finally語法,例如,若thisThrows為async函式,本體內容為throw new Error("Shit happens!!"),那麼,底下的程式可以捕捉例外,並顯示Error的訊息:

try {
await thisThrows();
} catch(e) {
console.log(e.message);
}

這只是個簡單示範,實際上,要考慮的情況還有更多,有興趣的話,我們可以進一步參考〈Error handling with Async/Await in JS〉。

簡單只是表象

在JavaScript的書籍中,很少探討錯誤處理,有的話也只是極少的篇幅,但千萬別被騙了,由於有著不同的環境實作,加上非同步的天性,JavaScript的錯誤處理,其實是比不少語言來得複雜。

或者應該說,錯誤處理從來就不是簡單的事,無論是在哪種語言或環境中,都應思考錯誤處理的差異性,而不單止於語言本身提供的錯誤處理機制是否簡單這件事上。

專欄作者

熱門新聞

Advertisement