深入理解 JavaScript 錯誤處理機制
作者包龍星(企業代號名),目前負責貝殼找房河圖專案的前端研發工作。
1 錯誤分類
javascript錯誤,可分為編譯時錯誤,執行時錯誤,資源載入錯誤。本文著重討論一下 執行時錯誤 和 資源載入錯誤 。
1.1 js執行時錯誤
javascript提供了一種捕獲執行時錯誤的捕獲機制。如果程式碼能夠捕獲潛在的錯誤,並能適當處理,就能確保程式碼不會在執行時產生意想不到的錯誤,給使用者造成困擾,這也意味著程式碼的質量是非常高的。
1.1.1 Error例項物件
javaScript解析或執行時,一旦發生錯誤,引擎就會丟擲一個錯誤物件。JavaScript原生提供Error建構函式,所有丟擲的錯誤都是這個建構函式的例項。
Error例項物件的三個屬性:
-
message 錯誤提示資訊
-
name 錯誤名稱
-
stack 錯誤的堆疊
例如下面的程式碼,列印錯誤例項物件,可以得到 message name stack 資訊:
1var err = new Error('出錯了'); 2console.dir(err)
上面的例子中, err 是一個物件( object )型別, 擁有 message、stack 兩個屬性,還有一個原型鏈上的屬性 name ,來自於建構函式 Error 的原型。
1.1.2 6種錯誤型別
以下6種錯誤型別都是Error物件的派生物件。在javascript中, 陣列array、函式function都是特殊的物件:
1)SyntaxError 語法錯誤
SyntaxError是程式碼解析時發生的語法錯誤。例如,寫了一個錯誤的語法 var a =
1function fn() { 2var a = 3} 4// Uncaught SyntaxError: Unexpected token } 5fn()
2)TypeError 型別錯誤
TypeError是變數或者引數不是預期型別時發生的錯誤。例如在number型別上呼叫array的方法。
1var n = 1234 2// Uncaught TypeError: a.concat is not a function 3a.concat(9)
3)RangeError 範圍錯誤
RangeError是一個值超過有效範圍發生的錯誤。例如設定陣列的長度為一個負值。
1// 陣列長度不得為負數 2new Array(-1) 3// Uncaught RangeError: Invalid array length
4)ReferenceError 引用錯誤
ReferenceError是引用一個不存在的變數時發生的錯誤。
1// Uncaught ReferenceError: mmm is not defined 2console.log(mmm)
5)EvalError eval錯誤
eval函式沒有被正確執行時,會丟擲EvalError錯誤。該錯誤型別已經不再使用了,只是為了保證與以前程式碼相容,才繼續保留。
1// Uncaught TypeError: eval is not a constructor 2new eval() 3// 不會報錯 4eval = () => {}
6)URIError URL錯誤
URIError指調 decodeURI encodeURI decodeURIComponent encodeURIComponent escape unescape 時發生的錯誤。
1// URIError: URI malformed 2at decodeURIComponent 3decode 4decodeURIComponent('%')
1.2 資源載入錯誤
當以下標籤(不包括 <link> ),載入資源出錯時,會發生資源載入錯誤。
1<img>, <input type="image">, <object>, <script>, <style> , <audio>, <video> 2
資源載入錯誤可以用onerror事件監聽。
1<img onerror="handleError">
資源載入錯誤不會冒泡,只能在事件流捕獲階段獲取錯誤。
1# 第三個引數預設為false, 設為true, 表示在事件流捕獲階段捕獲 2window.addEventListener('error', handleError, true)
當載入跨域資源時,不會報錯,需要在元素上新增 crossorigin,同時伺服器需要在response header中,設定Access-Control-Allow-Origin為*或者允許的域名。
1<script src="xxx" crossorigin></script>
2 錯誤捕獲
參考阿里開源框架jstracker原始碼
1// 阿里 jstracker 核心原始碼 2// 捕獲資源載入錯誤 3window.addEventListener('error', handleError, true) 4 5/** 6* 捕獲js執行時錯誤 7* 函式引數: 8* message: 錯誤資訊(字串) 9* source: 發生錯誤當指令碼URL 10* lineno: 發生錯誤當行號 11* colno: 發生錯誤當列號 12* error: Error物件 13**/ 14window.onerror = function(message, source, lineno, colno, error) { ... } 15 16// 捕獲vue中的錯誤, 重寫console.error 17console.error = () => {}
上面的程式碼, 不是很嚴謹, 如果使用者在程式碼中也寫了window.onerror, 會被覆蓋, 導致錯誤沒有正常上報。
3 throw
MDN關於throw的定義
throw語句用來丟擲一個使用者自定義的異常。當前函式的執行將被停止(throw之後的語句將不會執行),並且控制將被傳遞到呼叫堆疊中的第一個catch塊。如果呼叫者函式中沒有catch塊,程式將會終止。
MDN上關於throw的定義,翻譯得不夠準確,對於“程式將會終止”,我有不同的看法,下面請聽我的分析。
"throw 之後的語句將不會執行。",這句話比較容易理解,例如:
1console.log(1) 2throw 1234 3// 下面這行程式碼不會執行 4console.log(2)
"如果呼叫者函式中沒有catch塊,程式將會終止",這句話是有問題的。下面用程式碼來推翻這個結論:
1<button id="btn-1">列印1</button> 2<button id="btn-2">列印2</button> 3<script> 4function log(n) { 5console.log(n) 6} 7 8document.getElementById('btn-1').onclick = function() { 9log(1) 10} 11 12// 每1s列印一次 13setInterval(() => { 14log('setInterval依然在執行') 15}, 1000) 16 17throw new Error('手動丟擲異常') 18 19// 這段程式碼不會執行 20document.getElementById('btn-2').onclick = function() { 21log(2) 22} 23</script>
執行上面的程式碼,控制檯首先會丟擲錯誤,然後每秒列印"setInterval依然在執行"
點選btn-1,列印1;點選but-2,無反應。
這就說明:
throw 之後,程式沒有停止執行 。結論:throw之後的語句不會執行,並且控制將被傳遞到呼叫堆疊中的第一個catch塊。如果呼叫者函式中沒有catch塊,程式也不會停止,throw之前的語句依舊在執行。
4 try...catch...finally
try/catch的作用是將可能引發錯誤的程式碼放在try塊中,在catch中捕獲錯誤,對錯誤進行處理,選擇是否往下執行。
4.1 try 程式碼塊中的錯誤,會被catch捕獲,如果沒有手動丟擲錯誤,不會被window捕獲
1try { 2throw new Error('出錯了!'); 3} catch (e) { 4console.dir(e); 5throw e 6}
catch中丟擲異常,用 throw e ,不要用 throw new Error(e) ,因為 e 本身就是一個 Error 物件了,具有錯誤的完整堆疊資訊stack, new Error 會改變堆疊資訊,將堆疊定位到當前這一行。
4.2 try…finally… 不能捕獲錯誤
下面的程式碼,由於沒有catch,錯誤會直接被window捕獲。
1try { 2throw new Error('出錯啦啦啦') 3} finally { 4console.log('啦啦啦') 5}
4.3 try…catch…只能捕獲同步程式碼的錯誤,不能捕獲非同步程式碼錯誤
下面的程式碼,錯誤將不能被catch捕獲。
1try { 2setTimeout(() => { 3throw new Error('出錯啦!') 4}) 5} catch(e){ 6// 不會執行 7console.dir(e) 8}
因為setTimeout是非同步任務,裡面回撥函式會被放入到巨集任務佇列中,catch中程式碼塊屬於同步任務,處於當前的事件佇列中,會立即執行。(參考js事件迴圈機制:https://yangbo5207.github.io/wutongluo/ji-chu-jin-jie-xi-lie/shi-er-3001-shi-jian-xun-huan-ji-zhi.html)
當setTimeout中回撥執行時,try/catch中程式碼塊已不在堆疊中。所以錯誤不能被捕獲。
5 promise
Promise物件是JavaScript的一種非同步操作解決方案。Promise是建構函式,也是物件。
Promise的三種狀態:
-
pending 非同步操作未完成
-
fulfilled 非同步操作成功
-
rejected 非同步操作失敗
如果一個promise沒有resolve或reject,將一直處於pending狀態。
5.1 Promise的兩個方法
-
Promise.prototype.then 通常用來新增非同步操作成功的回撥
-
Promise.prototype.catch 用來新增非同步操作失敗的回撥
5.2 Promise內部的錯誤捕獲
用Promise可以解決“回撥地獄”的問題,但如果不能好處理Promise錯誤,將會陷入另一個地獄:錯誤將被“吞掉”,可能不會在控制檯列印,也不能被window捕獲。給除錯、線上故障排查帶來很大困難。
promise內部丟擲的錯誤, 都不會被window捕獲, 除非用了setTimeout/setInterval。
為了證明我的結論,我舉了一些例子:
例子1,錯誤會丟擲到控制檯,promise.catch回撥能夠執行,但錯誤不會被window捕獲。
1p = new Promise(()=>{ 2throw new Error('栗子1') 3}) 4 5p.catch((e) => { 6console.dir(e) 7})
例子2,p.then中但回撥函數出錯,錯誤會丟擲到控制檯,promise.catch回撥能夠執行,但錯誤不會被window捕獲。
1p = new Promise((resolve, reject) => { 2resolve() 3}) 4 5p.then(() => { 6throw new Error('栗子2') 7}).catch((e) => { 8console.dir(e) 9})
例子3,p.catch回調出錯,錯誤會丟擲到控制檯,後續的promise.catch回撥能夠執行,但錯誤不會被window捕獲。
1p = new Promise((resolve, reject) => { 2reject() 3}) 4 5p.catch(() => { 6throw new Error('栗子2') 7}).catch((e) => { 8console.dir(e) 9})
例子4,錯誤會丟擲到控制檯,後續的promise.catch回撥不會執行,錯誤會被window捕獲。
1p = new Promise((resolve, reject) => { 2reject() 3}) 4 5p.catch(() => { 6setTimeout((e) => { 7throw new Error('栗子2') 8}) 9}).catch((e) => { 10console.dir(e) 11})
例3和例4完全不一樣的結果,為什麼會這樣呢?因為promise內部也實現了類似於try/catch的錯誤捕獲機制,能夠捕獲錯誤。
參考promise 實現:https://github.com/then/promise/blob/master/src/core.js
1// es6實現的promise部分原始碼 2function Promise(fn) { 3... 4doResolve(fn, this); 5} 6 7function doResolve(fn, promise) { 8var done = false; 9var res = tryCallTwo(fn, function (value) { 10... 11}, function (reason) { 12... 13}); 14} 15 16function tryCallTwo(fn, a, b) { 17try { 18fn(a, b); 19} catch (ex) { 20LAST_ERROR = ex; 21return IS_ERROR; 22} 23}
從es6實現的promise可以發現, Promise() promise.then() promise.catch() 回撥函式執行時,都會被放到try…catch…中執行, 所以錯誤不能被 window.onerror 捕獲。而try…catch…包括setTimeout/setInterval 等非同步程式碼時,是不能捕獲到錯誤的。
5.3 在全域性捕獲promise錯誤
5.3.1 unhandledrejection 捕獲未處理Promise錯誤
用法:
1window.addEventListener('error', (e) => { 2console.log('window error', e) 3}, true) 4 5window.addEventListener('unhandledrejection', (e) => { 6console.log('unhandledrejection', e) 7}); 8 9let p = function() { 10return new Promise((resolve, reject) => { 11reject('出錯啦') 12}) 13} 14 15p()

相容性 :

unhandledrejection事件在瀏覽器中相容性不好,通常不這麼做。
6 async/await
當呼叫一個 async 函式時,會返回一個 Promise 物件。當這個 async 函式返回一個值時,Promise 的 resolve 方法會負責傳遞這個值;當 async 函式丟擲異常時,Promise 的 reject 方法也會傳遞這個異常值。
async/await的用途是簡化使用 promises 非同步呼叫的操作,並對一組 Promises執行某些操作。正如Promises類似於結構化回撥,async/await類似於組合生成器和 promises。
async 函式的返回值會被隱式的傳遞給 Promise.resolve
async函式內部的錯誤處理
async的推薦用法:
1async function getInfo1() { 2try { 3await ajax(); 4} catch (e) { 5// 錯誤處理 6throw e 7} 8}
await後面函式返回的promise的狀態有三種:
-
pending 非同步操作未完成
-
fulfilled 非同步操作成功
-
rejected 非同步操作失敗
async函式主體處理結果如下:
1)fulfilled 非同步操作成功
如果await後面函式返回的promise的狀態是fulfilled(成功),那程式將會繼續執行await後面到程式碼。下面的例子都是fulfilled狀態的。
1# demo 1: ajax success, no ajax().catch 2async function getInfo1() { 3try { 4await ajax(); 5console.log('123') 6} catch (e) { 7// 錯誤處理 8throw e 9} 10} 11 12# demo 2:ajax failed, ajax().catch do nothing 13async function getInfo1() { 14try { 15await ajax().catch(e => do nothing) 16console.log('123') 17} catch (e) { 18// 錯誤處理 19throw e 20} 21}
2)rejected 非同步操作失敗
如果await後面函式返回的promise的狀態是rejected(失敗),那程式將不會執行await後面的程式碼,而是轉到 catch 中到程式碼塊。下面的例子都是fulfilled狀態的。
1# demo 1: ajax failed 2async function getInfo1() { 3try { 4// ajax failed 5await ajax(); 6console.log('123') 7} catch (e) { 8// 錯誤處理 9throw e 10} 11} 12 13# demo 2:ajax failed, ajax().catch throw error 14async function getInfo1() { 15try { 16// ajax failed 17await ajax().catch(error => throw error) 18console.log('123') 19} catch (e) { 20// 錯誤處理 21throw e 22} 23}
3)pending 非同步操作未完成
如果await後面函式 ajax 沒有被 resolve 或 reject ,那麼將 ajax 一直處於pending狀態,程式將不會往後執行await 後面程式碼,也不能被catch捕獲,async函式也將一直處於pending狀態。
這樣的程式碼在我們身邊很常見,舉一個我遇到過的例子。
1function initBridge() { 2return new Promise((resolve, reject) => { 3window.$ljBridge.ready((bridge, webStatus) => { 4... 5resolve() 6}) 7}) 8} 9 10function async init(){ 11try{ 12await initBradge() 13// do something 14} catch(e) { 15throw e 16} 17} 18 19init()
上面的程式碼,initBradge由於沒有被正確當reject,當出錯時,將一直處於pending狀態。init內部即不能捕獲錯誤,也不能繼續往後執行,將一直處於pending狀態。
7 參考連結
1) 錯誤處理機制
(https://wangdoc.com/javascript/features/error.html)
2)js事件迴圈機制
(https://yangbo5207.github.io/wutongluo/ji-chu-jin-jie-xi-lie/shi-er-3001-shi-jian-xun-huan-ji-zhi.html)
作 者: 包龍星 (企業代號名)
出品人:漠北鷹、CC老師(企業代號名)
---------- END ----------
推薦閱讀