同步發佈於 https://github.com/xianshanna...javascript
個人建議是不要隱藏錯誤,勇敢地拋出來。沒有人會由於代碼出現 bug 致使程序崩潰而羞恥,咱們可讓程序中斷,讓用戶重來。錯誤是沒法避免的,如何去處理它纔是最重要的。
JavaScript 提供一套錯誤處理機制,錯誤是干擾程序正常流程的非正常的事故。而沒人能夠保持程序沒有 bug,那麼上線後遇到特殊的 bug,如何更快的定位問題所在呢?這就是咱們這個專題須要討論的問題。java
下面會從 JavaScript Error 基礎知識、如何攔截和捕獲異常、如何方便的在線上報錯誤等方面來敘述,本人也是根據網上的知識點進行了一些總結和分析(我只是互聯網的搬運工,不是創造者),若是有什麼錯漏的狀況,請在 issue 上狠狠的批評我。node
這個專題目前是針對瀏覽器的,還沒考慮到 node.js,不過都是 JavaScript Es6 語法,大同小異。git
通常分爲兩種狀況:github
大多數場景下咱們遇到的錯誤都是這類錯誤。若是發生Javscript 語法錯誤、代碼引用錯誤、類型錯誤等,JavaScript 引擎就會自動觸發此類錯誤。以下一些場景:web
場景一數組
console.log(a.notExited) // 瀏覽器會拋出 Uncaught ReferenceError: a is not defined
場景二promise
const a; // 瀏覽器拋出 Uncaught SyntaxError: Missing initializer in const declaration
語法錯誤,瀏覽器通常第一時間就拋出錯誤,不會等到執行的時候纔會報錯。瀏覽器
場景三網絡
let data; data.forEach(v=>{}) // Uncaught TypeError: Cannot read property 'forEach' of undefined
通常都是類庫開發的自定義錯誤異常(如參數等不合法的錯誤異常拋出)。或者從新修改錯誤 message 進行上報,以方便理解。
場景一
function sum(a,b){ if(typeof a !== 'number') { throw TypeError('Expected a to be a number.'); } if(typeof b !== 'number') { throw TypeError('Expected b to be a number.'); } return a + b; } sum(3,'d'); // 瀏覽器拋出 uncaught TypeError: Expected b to be a number.
場景二
固然咱們不必定須要這樣作。
let data; try { data.forEach(v => {}); } catch (error) { error.message = 'data 沒有定義,data 必須是數組。'; error.name = 'DataTypeError'; throw error; }
建立語法以下:
new Error([message[,fileName,lineNumber]])
省略 new
語法也同樣。
其中fileName
和 lineNumber
不是全部瀏覽器都兼容的,谷歌也不支持,因此能夠忽略。
Error
構造函數是通用錯誤類型,除了 Error
類型,還有 TypeError
、RangeError
等類型。
這裏列舉的都是 Error 層的原型鏈屬性和方法,更深層的原型鏈的繼承屬性和方便不作說明。一些有兼容性的並且不經常使用的屬性和方法不作說明。
console.log(Error.prototype) // 瀏覽器輸出 {constructor: ƒ, name: "Error", message: "", toString: ƒ}
其餘錯誤類型構造函數是繼承 Error
,實例是一致的。
錯誤信息, Error("msg").message === "msg"
。
錯誤類型(名字), Error("msg").name === "Error」
。若是是 TypeError,那麼 name 爲 TypeError。
Error
對象做爲一個非標準的棧屬性提供了一種函數追蹤方式。不管這個函數被被調用,處於什麼模式,來自於哪一行或者哪一個文件,有着什麼樣的參數。這個棧產生於最近一次調用最先的那次調用,返回原始的全局做用域調用。
這個不是規範,存在兼容性。經測試,谷歌、火狐、Edge、safar 都支持此特性(都是在最新的版本下測試 2019-04-02),IE 不支持。
返回值格式爲 ${name}: ${message}
。
除了通用的 Error
構造函數外,JavaScript還有常見的 5 個其餘類型的錯誤構造函數。
建立一個 Error 實例,表示錯誤的緣由:變量或參數不屬於有效類型。
throw TypeError("類型錯誤"); // Uncaught TypeError: 類型錯誤
建立一個 Error
實例,表示錯誤的緣由:數值變量或參數超出其有效範圍。
throw RangeError("數值超出有效範圍"); // Uncaught RangeError: 數值超出有效範圍
建立一個 Error
實例,表示錯誤的緣由:無效引用。
throw ReferenceError("無效引用"); // Uncaught ReferenceError: 無效引用
建立一個 Error
實例,表示錯誤的緣由:語法錯誤。這種場景不多用,除非類庫定義了新語法(如模板語法)。
throw SyntaxError("語法錯誤"); // Uncaught SyntaxError: 語法錯誤
建立一個 Error
實例,表示錯誤的緣由:涉及到 uri 相關的錯誤。
throw URIError("url 不合法"); // Uncaught RangeError: url 不合法
自定義新的 Error 類型須要繼承 Error ,以下自定義 CustomError
:
function CustomError(...args){ class InnerCustomError extends Error { name = "CustomError"; } return new InnerCustomError(...args); }
繼承 Error 後,咱們只須要對 name
作重寫,而後封裝成可直接調用的函數便可。
既然沒人能保證 web 應用不會出現 bug,那麼出現異常報錯時,如何攔截並進行一些操做呢?
這是攔截 JavaScript 錯誤,攔截後,若是不手動拋出錯誤,這個錯誤將靜默處理。日常寫代碼若是咱們知道某段代碼可能會出現報錯問題,就可使用這種方式。以下:
const { data } = this.props; try { data.forEach(d=>{}); // 若是 data 不是數組就會報錯 } catch(err){ console.error(err); // 這裏能夠作上報處理等操做 }
try...catch...
使用須要注意,try…catch… 後,錯誤會被攔截,若是不主動拋出錯誤,那麼沒法知道報錯位置。以下面的處理方式就是很差的。
function badHandler(fn) { try { return fn(); } catch (err) { /**noop,不作任何處理**/ } return null; } badHandler();
這樣 fn 回調發送錯誤後,咱們沒法知道錯誤是在哪裏發生的,由於已經被 try…catch 了,那麼如何解決這個問題呢?
function CustomError(...args){ class InnerCustomError extends Error { name = "CustomError"; } return new InnerCustomError(...args); } function uglyHandlerImproved(fn) { try { return fn(); } catch (err) { throw new CustomError(err.message); } return null; } badHandler();
如今,這個自定義的錯誤對象包含了本來錯誤的信息,所以變得更加有用。可是由於再度拋出來,依然是未處理的錯誤。
這個也要分場景,也看我的的理解方向,首先理解下面這句話:
try…catch 只會攔截當前執行環境的錯誤,try 塊中的異步已經脫離了當前的執行環境,因此 try…catch… 無效。
setTimeout
和 Promise
都沒法經過 try…catch 捕獲到錯誤,指的是 try 包含異步(非當前執行環境),不是異步包含 try(當前執行環境)。異步無效和有效 try…catch 以下:
setTimeout
這個無效:
try { setTimeout(() => { data.forEach(d => {}); }); } catch (err) { console.log('這裏不會運行'); }
下面的 try…catch 纔會有效:
setTimeout(() => { try { data.forEach(d => {}); } catch (err) { console.log('這裏會運行'); } });
Promise
這個無效:
try { new Promise(resolve => { data.forEach(d => {}); resolve(); }); } catch (err) { console.log('這裏不會運行'); }
下面的 try…catch 纔會有效:
new Promise(resolve => { try { data.forEach(d => {}); } catch (err) { console.log('這裏會運行'); } });
不是全部場景都須要 try…catch… 的,若是全部須要的地方都 try…catch,那麼代碼將變得臃腫,可讀性變差,開發效率變低。那麼我須要統一獲取錯誤信息呢?有沒有更好的處理方式?固然有,後續會提到。
Promise.prototype.catch
能夠達到 try…catch 同樣的效果,只要是在 Promise 相關的處理中報錯,都會被 catch 到。固然若是你在相關回調函數中 try…catch,而後作了靜默提示,那麼也是 catch 不到的。
以下會被 catch 到:
new Promise(resolve => { data.forEach(v => {}); }).catch(err=>{/*這裏會運行*/})
下面的不會被 catch 到:
new Promise(resolve => { try { data.forEach(v => {}); }catch(err){} }).catch(err=>{/*這裏不會運行*/})
Promise 錯誤攔截,這裏就不詳細說了,若是你看懂了 try…catch,這個也很好理解。
目前沒有相關的方式直接攔截 setTimeout 等其餘異步操做。
若是要攔截 setTimeout 等異步錯誤,咱們須要在異步回調代碼中處理,如:
setTimeout(() => { try { data.forEach(d => {}); } catch (err) { console.log('這裏會運行'); } });
這樣能夠攔截到 setTimeout 回調發生的錯誤,可是若是是下面這樣 try…catch 是無效的:
try { setTimeout(() => { data.forEach(d => {}); }); } catch (err) { console.log('這裏不會運行'); }
你可使用上面攔截錯誤信息的方式獲取到錯誤信息。可是呢,你要每一個場景都要去攔截一遍嗎?首先咱們不肯定什麼地方會發生錯誤,而後咱們也不可能每一個地方都去攔截錯誤。
不用擔憂,JavaScript 也考慮到了這一點,提供了一些便捷的獲取方式(不是攔截,錯誤仍是會終止程序的運行,除非主動攔截了)。
onerror 事件不管是異步仍是非異步錯誤(除了 Promise 錯誤),onerror 都能捕獲到運行時錯誤。
須要注意一下幾點:
addEventListener
,event.preventDefault()
能夠達到一樣的效果。<img/>
或<script>
資源加載失敗等錯誤。不過若是你使用了 fetch 等支持 promise 的方式,錯誤能夠經過 unhandledrejection 方式拿到錯誤信息。語法:
window.onerror
window.onerror = function(message, source, lineno, colno, error) { ... }
window.addEventListener('error')
window.addEventListener('error', function(event) { ... })
詳細看這裏。
unhandledrejection 存在兼容性問題,IE、Edge、火狐等目前都不支持。
用法以下:
window.addEventListener("unhandledrejection", event => { console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`); });
或者
window.onunhandledrejection = event => { console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`); };
詳細看這裏。
錯誤對象 Error
包含以下的字段返回:
錯誤的類型,通常有 Error
、TypeError
、ReferenceError
、RangeError
、URIError
等,固然也能夠是自定義的錯誤類型。
錯誤的詳細信息,能夠是 JavaScript 自動拋出的錯誤信息,也能夠是手動拋出的自定義信息。
這個不是規範,存在兼容性。經測試,谷歌、火狐、Edge、safar 都支持此特性(都是在最新的版本下測試 2019-04-02),IE 不支持。
name
和 message
咱們都不用作什麼處理,主要是要針對 stack 作處理,通常咱們須要把,這三個字段的信息提交到錯誤處理系統,針對性處理。
同時咱們生成環境的代碼是被壓縮後的代碼,須要使用 sourceMap 進行映射還原代碼。
後續會補充這個討論。