淺談JavaScript錯誤

本文主要從前端開發者的角度談一談大多數前端開發者都會遇到的js錯誤,對錯誤產生的緣由、發生階段,以及如何應對錯誤進行分析、概括和總結,但願獲得一些有益的結論用來指導平常開發工做。javascript

概念辨析

錯誤(Error)和異常(Exception)

對於Java來講錯誤和異常是兩個相近可是不一樣的概念,而在JavaScript中能夠認爲錯誤和異常是等同的,js裏只有Error關鍵字,並沒有Exception關鍵字。下文指的js錯誤也指一般理解的js異常。前端

js錯誤和bug

js錯誤:

一般是非程序設計的緣由致使的錯誤,大部分是發生在應用環境中的外部錯誤,好比硬件故障致使的I/O Error、網絡不穩定致使的Network Error,調用不被信任的外部方法,DOM操做,使用new Image、new FileReader加載資源。錯誤能夠被忽略或者捕獲,代碼能夠經過預設錯誤處理流程,例如使用了重試機制的代碼能夠經過重試有可能讓程序恢復到正常狀態。java

bug:

一般是程序設計的緣由致使的計算機程序或系統中的缺陷,可以引起錯誤或意外結果,或使程序或系統以非預期方式運行。一般沒法繼續和恢復,須要程序員進入程序而且修改代碼來修復。程序員

javaScript錯誤發生階段

因爲JavaScript語言解釋型的特性,js錯誤發生在運行時,這一點和編譯型語言相比錯誤更加難以發現。幸運的是技術發展到今天已經有很是多成熟的工具好比Eslint、IDE的代碼檢查,能夠幫助咱們在早期不須要運行程序的階段發現錯誤。還有一些JavaScript語言的超集語言,爲語言添加了可選的靜態類型,也能夠幫助在早期發現錯誤。程序運行開始後,在進行用戶交互以前,一些語法錯誤,常見的好比Uncaught SyntaxError是很是容易發現的。而剩下的那些,基本上須要經過用戶交互來觸發,較難以發現,本文着重討論這部分錯誤。bash

錯誤的幾種應對方式

不捕獲錯誤

若是判斷程序當前位置可能會發生錯誤,不捕獲錯誤是一種消極的應對方式。依賴全局window.onerror錯誤監聽可以獲取未捕獲的錯誤信息。服務器

捕獲錯誤,不處理,拋出

若是捕獲錯誤後立刻拋出,拋出的error對象就是原來的對象,至關於啥也沒幹,等同於不捕獲錯誤。通常來講採用這種應對方式時都會幹點什麼,好比拋出一個自定義的錯誤,而不是原始錯誤對象。網絡

捕獲錯誤,不處理,不拋出

要當心不處理不拋出意味着全局window.onerror錯誤監聽也沒法獲取到該錯誤信息,通常來講這種應對方式用在一些不影響程序主流程的錯誤處理上,好比調用DOM節點的focus方法,但仍是須要註釋合理的理由。 捕獲錯誤後靜默處理的示例:併發

try {
    obj[0].focus();
} catch (e) {
    // IE8 can throw "Can't move focus to the control because it is invisible, // not enabled, or of a type that does not accept the focus." for all kinds of
    // reasons that are too expensive and fragile to test.
}
複製代碼

捕獲錯誤,處理

一般會在處理方法體中使用錯誤日誌上報、失效保護、重試恢復等技術。失效保護即降級處理,舉一個比較簡明的例子,try { a = JSON.parse(b); } catch (e) { a = {}; }異步

幾個引伸出的問題

不處理錯誤形成的影響?

這個問題缺少前提,究竟是捕獲了不處理仍是未捕獲錯誤。前者已有解答,這裏說未捕獲錯誤可能會形成的影響。錯誤發生後,在出錯位置以前的代碼已經執行過了,以後的代碼再也不執行,這種狀況基本上就是bug了。因爲js的併發模型與事件循環機制,若是在出錯位置以前執行過異步代碼,好比setTimeout、new Promise,異步代碼中的回調函數仍然會在「執行棧」中的全部同步任務執行完畢以後按照回調函數在任務隊列裏的順序執行。值得注意的是,利用這種特性,能夠將錯誤包裝在一個異步執行的函數中使用異步拋出錯誤的技術,可以避免阻斷錯誤處以後的代碼執行。 異步拋出錯誤示例:async

const asyncThrowError = (error) => {
    setTimeout(function() {
        throw error || new Error('異步拋出的異常');
    });
};
try {
    let a = JSON.parse('{a: a}');
} catch(e) {
    // 使用異步拋出異常的技術,依靠window.onerror捕獲異常並記錄日誌
    asyncThrowError(e);
}
複製代碼

何時應該捕獲錯誤?

固然是預感程序某一處可能會出現問題的時候啦。具體何時則見仁見智啦,依賴程序員的經驗。

何時應該拋出錯誤,錯誤拋出後會怎麼樣?

人爲主動拋出的錯誤和應用環境中的發生的錯誤一樣會致使錯誤位置以後的代碼沒法執行。常見的一種用法是在函數檢查傳入的參數是否合法,不合法就使程序快速失效(Fail Fast),提醒開發者修復問題。還能夠將拋出錯誤技術用在收集用戶填寫的不合法的表單數據上,在內層代碼中拋出錯誤,在外層代碼中捕獲錯誤並判斷錯誤類型,獲取到錯誤信息。

錯誤的重試與恢復

若是前提得是可恢復類型的錯誤,在程序中加入重試機制纔有意義。可恢復的錯誤一般是外部緣由致使的,典型例子如網絡錯誤。重試機制根據是否須要用戶交互觸發可分爲自動重試和手動重試。自動重試的一種設計是,經過線性增加的間隔時間或者成指數增加的間隔時間循環重試直到沒有錯誤發生,尤爲是指數增加的間隔時間循環重試機制能夠避免程序太快將計算機資源佔滿。手動重試的一個設計例子是,在一些關鍵業務流程,好比電商場景中的添加到購物車,當接口返回失敗時,能夠經過讓用戶手動點擊重試按鈕觸發新的一輪接口調用。 自動重試的一種設計方案:

const sendMesg = (errorResendTimes = 0) => {
    console.log(errorResendTimes);
    try {
        throw 'hahaha';
    } catch (e) {
        setTimeout(
            () => sendMesg(errorResendTimes), 
            100 * Math.pow(2, errorResendTimes) // 使用指數增加的時間間隔
            // 100 * errorResendTimes // 使用線性增加的時間間隔
        );
        errorResendTimes++;
    }
};
複製代碼

錯誤的隔離與降級

這個是js中捕獲並處理錯誤的最核心的出發點,有經驗的程序員爲了不未捕獲的錯誤阻斷程序執行形成bug,須要在程序中設置陷阱(使用try/catch語句)捕獲錯誤。當錯誤發生的時候,使用了錯誤捕獲技術的地方能夠防止錯誤影響當前調用棧以後的代碼執行,隔離了錯誤的影響範圍,將影響範圍限制在當前函數做用域。錯誤的降級處理指的是當錯誤發生後,雖然已使用錯誤捕獲技術隔離了影響範圍,可是假如當前調用棧以後的代碼仍然依賴當前的執行結果,錯誤致使執行結果爲非預期的數據類型,那麼以後的代碼使用該執行結果必然又會出錯。 錯誤的降級處理示例:

let a;
try {
    a = JSON.parse('{a: a}');
} catch (e) {
    a = {};
}
console.log(a.a);
複製代碼

如何上報錯誤信息到日誌服務器

這個問題分爲兩個步驟,首先要獲取到錯誤信息,其次是預處理這些錯誤信息,一般狀況下使用一些技術手段合併、降頻,最後再上報日誌服務器。這裏着重談如何獲取到更全面細緻的錯誤信息。直接看示例:

// 監聽js運行時異常
window.onerror = (message, source, lineno, colno, error) => {
    var errorMesg = wrapError({message, source, lineno, colno, error});
    console.log(message)
    sendErrorToServer('js運行時異常:' + errorMesg);
}

// 監聽document資源加載異常
window.addEventListener('error', function(e) {
    // 過濾非window上捕獲的異常
    if (e.target === window) {
        return;
    }
    var errorMesg = wrapError(e);
    sendErrorToServer('document資源加載異常:' + errorMesg);
}, true);

// 監聽未捕獲的Promise異常
window.addEventListener('unhandledrejection', function(e) {
    var errorMesg = e.reason;
    sendErrorToServer('未捕獲的Promise異常:' + errorMesg);
});

// 監聽最初未捕獲稍後又被捕獲的Promise異常
window.addEventListener('rejectionhandled', function(e) {
    var errorMesg = e.reason;
    sendErrorToServer('最初未捕獲稍後又被捕獲的Promise異常:' + errorMesg);
});

// 已捕獲的Promise異常
new Promise(function(resolve, reject) {
    // reject('我被捕獲了');
    throw new Error('我被捕獲了');
}).catch(function(reason) {
    var errorMesg = reason;
    sendErrorToServer('已捕獲的Promise異常:' + errorMesg);
});

// 未捕獲的Promise異常
new Promise(function(resolve, reject) {
    reject('我沒有被捕獲');
});

// 最初未捕獲稍後又被捕獲的Promise異常:
var p1 = new Promise(function(resolve, reject) {
    reject('我後來被捕獲了');
});
setTimeout(function(){
    p1.catch(function(e) {
    });
}, 200);

function sendErrorToServer(error) {
    const img = new Image();
    var from = window.location.href;
    error += '\n' + from;
    img.src = "http://www.baidu.com?error=" + error;
}
function wrapError({message='', source='', lineno='', colno='', error, target}) {
    var returned = '';
    var mesg = error && error.toString() || message;
    var stack = error && error.stack || '';
    returned += '\n' + mesg;
    returned += '\n' + source;
    returned += '\n' + lineno;
    returned += '\n' + colno;
    returned += '\n' + stack;
    if (target && target.outerHTML) {
        returned += '\n' + target.outerHTML;
    }
    return returned;
}
複製代碼

名詞解釋

捕獲錯誤:

使用try/catch塊的try語句將可能出錯的代碼包含進去。

處理錯誤:

在try/catch塊的catch語句中存在非註釋的代碼邏輯能夠認爲錯誤通過處理。

不處理錯誤:

與之相反,catch(e) {}內部沒有代碼邏輯,能夠認爲錯誤沒有通過處理。

拋出錯誤:

使用throw語句後跟錯誤對象,throw new Error('error message')throw 'error message'

最後發個廣告,網易七魚招聘前端開發工程師,挑戰高薪傳送門

相關文章
相關標籤/搜索