在前端的 JAVAScript 開發(fā)中,發(fā)現(xiàn)開發(fā)者對于錯誤異常的處理普遍都比較簡單粗暴,如果應(yīng)用程序中缺少有效的錯誤處理和容錯機(jī)制,代碼的健壯性就無從談起。本文整理出了一些常見的錯誤異常處理的場景,旨在為前端的 JavaScript 錯誤異常處理提供一些基礎(chǔ)的指導(dǎo)。
Error 對象
先來簡單介紹一下 JavaScript 中的 Error 對象,通常 Error 對象由重要的兩部分組成,包含了 error.message 錯誤信息和 error.stack 錯誤追溯棧。
產(chǎn)生一個錯誤很簡單,比如在 foo.js 中直接調(diào)用一個不存在的 callback 函數(shù)。
// foo.jsfunction foo () { callback();}foo();
此時通過 Chrome 瀏覽器的控制臺會展示如下的信息。
Uncaught ReferenceError: callback is not defined at foo (foo.js:2) at foo.js:5
其中 Uncaught ReferenceError: callback is not defined 就是 error.message 錯誤信息,而剩下的 at xxx 就是具體的錯誤追溯棧,在 Chrome 的控制臺中,對錯誤的展示進(jìn)行了優(yōu)化。
如果我們通過 window.onerror 來捕獲到該錯誤后將 Error 對象直接輸出到頁面中會展示出更原始的數(shù)據(jù)。
<!-- 展示錯誤的容器 --><textarea id="error"></textarea>// 輸出錯誤window.onerror = function (msg, url, line, col, err) { document.getElementById('error').textContent = err.message + 'nn' + err.stack;};
原始的錯誤數(shù)據(jù)中會展示出錯誤追溯棧中的 Source URL。
callback is not definedReferenceError: callback is not defined at foo (http://example.com/js-error/foo.js:2:5) at http://example.com/js-error/foo.js:5:1
有了錯誤追溯棧,就能通過發(fā)生錯誤的文件 Source URL 和錯誤在代碼中的具體位置來快速定位到錯誤。
看起來好像很簡單,但實(shí)際的開發(fā)中如何有效的捕獲錯誤,如何有效的拋出錯誤都有一些需要注意的點(diǎn),下面逐個的來講解。
window.onerror
前端在捕獲錯誤時都會通過綁定 window.onerror 事件來捕獲全局的 JavaScript 執(zhí)行錯誤,標(biāo)準(zhǔn)的瀏覽器在響應(yīng)該事件時會依次提供 5 個參數(shù)。
window.onerror = function(message, source, lineno, colno, error) { ... }
- message 錯誤信息
- source 錯誤發(fā)生時的頁面 URL
- lineno 錯誤發(fā)生時的 JS 文件行數(shù)
- colno 錯誤發(fā)生時的 JS 文件列數(shù)
- error 錯誤發(fā)生時拋出的標(biāo)準(zhǔn) Error 對象
使用 window.addEventListener 也能綁定 error 事件,但是該事件函數(shù)的參數(shù)是一個 ErrorEvent 對象。
綁定 window.onerror 事件時,事件處理函數(shù)的第 5 個參數(shù)在低版本瀏覽中或 JS 資源跨域場景下可能不是 Error 對象。
在 Chrome 瀏覽器中如果頁面加載的 JS 資源文件中存在跨域的 script 標(biāo)簽,在發(fā)生錯誤時會提示 Script error 而缺乏錯誤追溯棧。
window.onerror 在響應(yīng)跨域 JavaScript 錯誤時缺乏錯誤追溯棧時的 arguments 對象如下:
[ 'Script error.', '', 0, 0, null]
為了正常的捕獲到跨域 JS 資源文件的錯誤,需要具備兩個條件: 1. 為 JS 資源文件增加 CORS 響應(yīng)頭。 2. 通過 script 引用該 JS 文件時增加 crossorigin="anonymous" 的屬性,如果是動態(tài)加載的 JS,可以寫作 script.crossOrigin = true 。
window.onerror 能捕獲一些全局的 JavaScript 錯誤,但還有不少場景在全局是捕獲不到的。
try/catch
window.onerror 能捕獲全局場景下的錯誤,如果已知一些程序的場景中可能會出現(xiàn)錯誤,這個時候一般會使用 try/catch 來進(jìn)行捕獲。
但是在使用 try/catch 塊時無法捕獲異步錯誤,例如塊中使用了 setTimeout 。
try { setTimeout(function () { callTimeout(); // callTimeout 未定義,會拋錯 }, 1000);}catch (err) { console.log('catch the error', err); // 不會被執(zhí)行}
try/catch 在處理 setTimeout 這類異步場景時是無效的,執(zhí)行時仍會拋錯,catch 中的代碼不會被執(zhí)行。
雖然在 try/catch 中沒有捕獲到,此時如果有綁定 window.onerror 則會被全局捕獲。
由此可見, try/catch 應(yīng)該是只能捕獲 JS Event Loop 中同步的任務(wù)。
如果想正確的捕獲 setTimeout 中的錯誤,需要將 try/catch 塊寫到 setTimeout 的函數(shù)中。
setTimeout(function () { try { callTimeout(); // callTimeout 未定義,不會拋錯 } catch (err) { console.log('catch the error', err); // 將會被執(zhí)行 }}, 1000);
Promise
Promise 有自己的錯誤處理機(jī)制,通常 Promise 函數(shù)中的錯誤無法被全局捕獲。
var promise = new Promise(executor);promise.then(onFulfilled, onRejected);
比較容易遺漏錯誤處理的地方有 executor 和 onFulfilled ,在這些函數(shù)中如果發(fā)生錯誤都不能被全局捕獲。
正確的捕獲 Promise 的錯誤,應(yīng)該使用 Promise.prototype.catch 方法,意外的錯誤和使用 reject 主動捕獲的錯誤都會觸發(fā) catch 方法。
catch 方法中通常會接收到一個 Error 對象,但是當(dāng)調(diào)用 reject 函數(shù)時傳入的是一個非 Error 對象時,catch 方法也會接收到一個非 Error 對象,這里的 reject 和 throw 的表現(xiàn)是一樣的,所以在使用 reject 時,最好是傳入一個 Error 對象。
reject( new Error('this is reject message'));
值得注意的是,如果 Promise 的 executor 中存在 setTimeout 語句時, setTimeout 的報錯會被全局捕獲。
Async Function
Async Function 和 Promise 一樣,發(fā)生錯誤不會被全局的 window.onerror 捕獲,所以在使用時如果有報錯,需要手動增加 try/catch 語句。
匿名函數(shù)
匿名函數(shù)的使用在 JavaScript 中很常見,但是當(dāng)出現(xiàn)匿名函數(shù)的報錯時,在錯誤追溯棧中會以 anonymous 來標(biāo)識錯誤,為了排查錯誤方便,可以將函數(shù)進(jìn)行命名,或者使用函數(shù)的 displayName 屬性。
函數(shù)如果有 displayName 屬性,在錯誤棧中會展示該屬性值,如果用于命名重要的業(yè)務(wù)邏輯屬性,將有效幫助排查錯誤。
throw error
上面說了很多錯誤捕獲的注意點(diǎn),如果要主動的拋錯,都會使用 throw 來拋錯,常見的幾種拋錯方法如下:
throw new Error('Problem description.') // 方法 1throw Error('Problem description.') // 方法 2throw 'Problem description.' // 方法 3throw null // 方法 4
其中方法 1 和方法 2 的效果一樣,瀏覽器都能正確的展示錯誤追溯棧。方法 3 和方法 4 不推薦,雖然能拋錯,但是在拋錯的時候不能展示錯誤追溯棧。
try/catch 和 throw ,一個用來捕獲錯誤,一個用來拋出錯誤,如果兩個結(jié)合起來用通常等于脫了褲子放屁多此一舉,唯一有點(diǎn)用的是可以對錯誤信息進(jìn)行再加工。
可以在 Chrome 控制臺中模擬出一個結(jié)合使用的實(shí)際場景。
try { foo();}catch (err) { err.message = 'Catch the error: ' + err.message; throw Error(err);}
由于在 catch 塊中又拋出了錯誤,所以該錯誤沒有被捕獲到,但此時錯誤信息經(jīng)過了二次封裝。
Uncaught Error: ReferenceError: Catch the error: foo is not defined
通過對錯誤信息的二次封裝,可以增加一些有利于快速定位錯誤的額外信息。
原作者:雨夜帶刀's Blog