【原譯】javascript 中的正確錯誤處理
摘要
這是關于JavaScript中異常處理的故事。如果你相信墨菲定律,那么任何事情都可能出錯,不,一定會出錯!這篇文章中我們來看下JavaScript中的出錯處理。文章會覆蓋異常處理使用的正反例,然后看下ajax的異步處理。
JavaScript的事件驅動機制讓JavaScript更加豐富,瀏覽器好比就是一個事件驅動的機器,錯誤也是一種事件。當一個錯誤發生時,一個事件就在某個點拋出。理論上,有人會說錯誤是Javascript中的簡單事件。如果你覺得是這樣,那你就要好好去看看了。另外這篇文章只關注瀏覽器端的JavaScript的情況。
這篇文章將在《 Exceptional Exception Handling in JavaScript 》這篇文章的概念基礎上進行解釋。解釋起來就是,當發生錯誤時,JavaScript會去調用棧檢查異常事件。如果你對此不熟悉建議先去看看基礎的東西。我們的目的是探索處理異常的必要性,接下來你會看到一個 try...catch 塊語句,你要認真思考。
例子
例子的代碼在 github 上,而且最終展示成這樣:
所有的按鈕點擊是都會觸發"炸彈",這個炸彈模擬了一個拋出的 TypeError 異常。下面是這個模塊單元測試的定義:
function error() { var foo = {}; return foo.bar();
}
開始時,這個函數定義了一個空的對象 foo ,注意 bar() 沒有在任何地方定義,我們用一個測試用例來看下它是如何引爆炸彈的。
it('throws a TypeError', function () {
should.throws(target, TypeError);
});
這個單元測試是用 mocha 和 should.js 寫的。 mocha 是一個測試框架, should.js 是一個斷言庫。如果你熟悉它們后,你會感覺寫起來很爽。測試一般使用 it('description') 開始,然后在 should 中使用 pass/fail 結束。好消息是測試用例可以在node端運行而不需要瀏覽器。我建議多關注這些測試,因為它們能幫助我們提升代碼的質量。
正如所顯示的, error() 定義了一個空的對象,然后嘗試訪問一個方法,因為 bar() 方法在對象中不存在而會拋出一個異常。使用JavaScript這種動態語言運行一定會出錯。
錯誤的方式
對于一些錯誤的處理,我從按鈕的而事件中抽離出異常處理的方式,下面是單元測試函數的代碼:
function badHandler(fn) { try { return fn();
} catch (e) { } return null;
}
這個處理函數接收一個 fn 回調函數作為輸入,這個函數然后在處理器函數里面被調用,單元測試如下:
it('returns a value without errors', function() { var fn = function() { return 1;
}; var result = target(fn); result.should.equal(1);
});
it('returns a null with errors', function() { var fn = function() {
throw Error('random error');
}; var result = target(fn);
should(result).equal(null);
});</code></pre>
如你所見,這個糟糕的處理函數如果有地方出錯就會返回null,回調函數 fn() 可以指向一個正確的方法或者一個異常,下面的點擊處理函數會顯示最終的處理結果。
(function (handler, bomb) {
var badButton = document.getElementById('bad');
if (badButton) {
badButton.addEventListener('click', function () {
handler(bomb);
console.log('Imagine, getting promoted for hiding mistakes');
});
}}(badHandler, error));</code></pre>
可惡的是,這里返回了一個null,當我想找哪里出了問題時整個人都蒙逼了。這種失敗沉默的方式會影響用戶體驗和數據混亂。更令人崩潰的是,我花了幾個小時來進行debugg,但卻沒有使用 try-catch ,這個糟糕的處理函數吞沒了錯誤并認為它沒有問題, 這樣繼續執行下去不會降低代碼質量,但是隱藏的錯誤未來會讓你花幾個小時來debugg。在一個多層的深調用時,基本上不可能發現哪里出了問題。而在這些少數的地方使用 try-catch 是正確的。但是一旦進入錯誤處理函數,就比較糟糕了。
失敗沉默策略會讓你不容易發現錯誤所在,JavaScript提供了一個更優雅的方式來處理這些問題。
比較差的方式
繼續,是時候說下一個稍微好點的方法了。我先跳過事件綁定到dom上的部分。這個函數處理和剛剛我們看到的沒什么不同。所不同的是單元測試中它處理異常的方式。
function uglyHandler(fn) { try { return fn();
} catch (e) { throw Error('a new error');
}
}
it('returns a new error with errors', function () { var fn = function () { throw new TypeError('type error');
};
should.throws(function () {
target(fn);
}, Error);
});
這里定義在原來的基礎上改進了。這里異常事件在調用棧中進行冒泡,我喜歡的是現在錯誤現在會離開方便debugg的調用棧。在這個異常中,解釋器會遍歷整個棧尋找另一個錯誤處理函數。這樣就可以有機會在調用棧的頂端處理這些錯誤。不幸的是,因為這個方法,我不知道錯誤是從哪個地方拋出來的。所以我又得反向遍歷這個棧找到錯誤異常的源頭。但至少我知道某個地方出錯了,并能找到是哪個地方拋出的錯誤。
離開調用棧
所以,一個拋出異常處理的方法是直接調用棧的頂端使用 try-catch ,就像:
function main(bomb) { try {
bomb();
} catch (e) { // Handle all the error things
}
}
但是,記住我說的瀏覽器是事件驅動的。是的,JavaScript中的錯誤也不過是一個事件。解釋器在當前的執行上下文中執行后釋放。結果是,我們可以利用一個 onerror 的全局異常事件處理函數,它大概是這樣的:
if(window.addEventListener){ window.addEventListener('error', function (e) { var error = e.error;
console.log(error);
});
}else if(window.attachEvent){ window.attachEvent('onerror', function (e) { var error = e.error;
console.log(error);
});
}else{ window.onerror = function(e){ var error = e.error;
console.log(error);
}
}
這個處理函數能捕獲任何執行上下文中的錯誤異常。包括任何類型的任何錯誤。而且它能定位到代碼中的錯誤處理。就像其它任何事件一樣,你能捕獲特定錯誤的具體信息。這樣能使異常處理器只專注于一件事情,如果你允許這樣做的話。這些處理函數也可以在任何時候注冊,解釋器會盡可能的遍歷更多的處理函數,我們再也不用使用 try-catch 塊這種帶有瑕疵的debug方式了。尤其是在對待像JavaScript這類事件驅動機制的語言時,onerror的優勢就更大了
現在我們可以使用全局處理函數來離開棧了,我們可以用來干什么呢。畢竟,調用棧還是存在的。
捕獲棧信息
調用棧在定位問題時超級有用。好消息是,瀏覽器提供了這個信息。理所當然,查看錯誤異常中的棧屬性不是標準的一部分,但是只在新的瀏覽器中可以使用。所以,你就可以這樣來把錯誤日志發送給服務器了。
window.addEventListener('error', function (e) { var stack = e.error.stack; var message = e.error.toString(); if (stack) {
message += '\n' + stack;
} var xhr = new XMLHttpRequest();
xhr.open('POST', '/log', true);
xhr.send(message);
});
可能從代碼樣例來說不是很明顯,但是上面的代碼一定會出錯。上面提到了,每個處理函數都只處理一個功能。我關心的是這些信息是怎樣被服務器捕獲的。如下:

這些信息來自FireFox 46的開發版本,通過一個正確的錯誤處理函數,記錄了出錯的情況。這里沒必要隱藏錯誤,我可以看到什么地方出現的什么錯誤。這樣代碼debugg就很爽了。這些信息也可以保存在持續化緩存中以便于以后分析。
調用棧對于debugg來說是很有用的,永遠不要低估調用棧的力量。
異步處理
處理異步時,JavaScript的異步處理代碼不在當前的指向上下文中,這意味著 try-catch 語句會有問題(不能捕獲到異常):
function asyncHandler(fn) { try {
setTimeout(function () {
fn();
}, 1);
} catch (e) { }
}
單元測試的結果如下:
it('does not catch exceptions with errors', function () { var fn = function () { throw new TypeError('type error');
};
failedPromise(function() {
target(fn);
}).should.be.rejectedWith(TypeError);
});function failedPromise(fn) { return new Promise(function(resolve, reject) {
reject(fn);
});
}
我必須用promise包含這個處理器來獲取這個錯誤。注意的是,一個未被處理的異常發生時,盡管我將代碼使用 try-catch 包含起來了,是的, try-catch 只能在單一的作用域內有效。在一個異常被拋出的同時,解釋器就會從 try-catch 中離開,ajax也是一樣的。所以有兩種選擇,一種是在異步調用里面捕獲異常:
setTimeout(function () { try {
fn();
} catch (e) { // Handle this async error
}
}, 1);
這種方法很有效,但是很多地方可以改進。首先, try-catch 塊在這里用很混亂。實際上,之前是這么做的,但是有問題。另外,V8引擎不鼓勵 在函數中使用try-catch (V8 是chrome和nodejs中的JavaScript引擎)。它們的建議是最外層寫這些塊。
所以我們該怎么辦?我說過全局異常處理可以在任何執行上下文中執行,如果給window對象增加一個錯誤處理函數,就OK了。這樣是不是既能處理捕獲處理錯誤又能保持代碼的優雅呢。全局的錯誤處理能讓你的代碼干凈整潔。
下面是服務器收集到的錯誤日志,注意的是如果你使用同樣的代碼再不同瀏覽器上執行,你會看到收集到的日志也是不同的:

這個處理函數甚至告訴我們錯誤是從異步代碼中拋出的嗎,它告訴我們來至 setTimeout() 函數。
結論
總得來說,進行異常處理至少有兩種方法。一個是失敗沉默的方法,在錯誤發生時忽略錯誤不作為而不影響后面的繼續執行。另一種是發生后迅速找到錯誤發生的地方。明顯我們知道那種方法更具有優勢。我的選擇是:不要隱藏錯誤。沒人會因為你代碼中有問題而鄙視你,用戶多試一次是可以接受的。代碼距離完美是很遠的,錯誤也是不可避免的,重要的是你發現錯誤后會怎么做。
來自:http://mp.weixin.qq.com/s?__biz=MzAwNzA0NTMzMQ==&mid=2653201956&idx=1&sn=f7b79ca27fffcaccfac71e8802d0bb37&scene=4#wechat_redirect