本文約 8000 字,預(yù)計閱讀需要 25 分鐘。 大家好,我是 CUGGZ。 本文將帶你了解 JavaScript 中常見的錯誤類型,處理同步和異步 JavaScript/Node.js 代碼中錯誤和異常的方式,以及錯誤處理最佳實踐! 1. 錯誤概述JavaScript 中的錯誤是一個對象,在發(fā)生錯誤時會拋出該對象以停止程序。在 JavaScript 中,可以通過構(gòu)造函數(shù)來創(chuàng)建一個新的通用錯誤: const err = new Error('Error'); 當(dāng)然,也可以省略 new 關(guān)鍵字:
Error 對象有三個屬性:
例如,創(chuàng)建一個 TypeError 對象,該消息將攜帶實際的錯誤字符串,其 name 將是“TypeError”: const wrongType = TypeError('Expected number'); 堆棧跟蹤是發(fā)生異?;蚓娴仁录r程序所處的方法調(diào)用列表:它首先會打印錯誤名稱和消息,然后是被調(diào)用的方法列表。每個方法調(diào)用都說明其源代碼的位置和調(diào)用它的行??梢允褂么藬?shù)據(jù)來瀏覽代碼庫并確定導(dǎo)致錯誤的代碼段。此方法列表以堆疊的方式排列。它顯示了異常首先被拋出的位置以及它如何通過堆棧方法調(diào)用傳播。為異常實施捕獲不會讓它通過堆棧向上傳播并使程序崩潰。 對于 Error 對象,F(xiàn)irefox 還實現(xiàn)了一些非標(biāo)準屬性:
2. 錯誤類型JavaScript 中有一系列預(yù)定義的錯誤類型。只要使用者沒有明確處理應(yīng)用程序中的錯誤,它們就會由 JavaScript 運行時自動選擇和定義。 JavaScript中的錯誤類型包括:
這些錯誤類型都是實際的構(gòu)造函數(shù),旨在返回一個新的錯誤對象。最常見的就是 TypeError。大多數(shù)時候,大部分錯誤將直接來自 JavaScript 引擎,例如 InternalError 或 SyntaxError。 JavaScript 提供了
下面來了解 JavaScript 中最常見的錯誤類型,并了解它們發(fā)生的時間和原因。 (1)SyntaxErrorSyntaxError 表示語法錯誤。這些錯誤是最容易修復(fù)的錯誤之一,因為它們表明代碼語法中存在錯誤。由于 JavaScript 是一種解釋而非編譯的腳本語言,因此當(dāng)應(yīng)用程序執(zhí)行包含錯誤的腳本時會拋出這些錯誤。在編譯語言的情況下,此類錯誤在編譯期間被識別。因此,在修復(fù)這些問題之前,不會創(chuàng)建應(yīng)用程序二進制文件。 SyntaxError 發(fā)生的一些常見原因是:
(2)TypeErrorTypeError 是 JavaScript 應(yīng)用程序中最常見的錯誤之一,當(dāng)某些值不是特定的預(yù)期類型時,就會產(chǎn)生此錯誤。TypeError 發(fā)生的一些常見原因是:
(3)ReferenceErrorReferenceError 表示引用錯誤。當(dāng)代碼中的變量引用有問題時,會發(fā)生 ReferenceError??赡芡浽谑褂米兞恐盀槠涠x一個值,或者可能試圖在代碼中使用一個不可訪問的變量。在任何情況下,通過堆棧跟蹤都可以提供充足的信息來查找和修復(fù)有問題的變量引用。 ReferenceErrors 發(fā)生的一些常見原因如下:
(4)RangeErrorRangeError 表示范圍錯誤。當(dāng)變量設(shè)置的值超出其合法值范圍時,將拋出 RangeError。它通常發(fā)生在將值作為參數(shù)傳遞給函數(shù)時,并且給定值不在函數(shù)參數(shù)的范圍內(nèi)。當(dāng)使用記錄不完整的第三方庫時,有時修復(fù)起來會很棘手,因為需要知道參數(shù)的可能值范圍才能傳遞正確的值。 RangeError 發(fā)生的一些常見場景如下:
(5)URIErrorURIError 表示 URI錯誤。當(dāng) URI 的編碼和解碼出現(xiàn)問題時,會拋出 URIError。JavaScript 中的 URI 操作函數(shù)包括: (6)EvalErrorEvalError 表示 Eval 錯誤。當(dāng) 如果使用的是舊版本的 JavaScript,可能會遇到此錯誤。在任何情況下,最好調(diào)查在eval()函數(shù)調(diào)用中執(zhí)行的代碼是否有任何異常。 (7)InternalErrorInternalError 表示內(nèi)部錯誤。在 JavaScript 運行時引擎發(fā)生異常時使用。它表示代碼可能存在問題也可能不存在問題。 InternalError 通常只發(fā)生在兩種情況下:
解決此錯誤最合適的方法就是通過錯誤消息確定原因,并在可能的情況下重構(gòu)應(yīng)用邏輯,以消除 JavaScript 引擎上工作負載的突然激增。 注意: 現(xiàn)代 JavaScript 中不會拋出 EvalError 和 InternalError。 (8)創(chuàng)建自定義錯誤類型雖然 JavaScript 提供了足夠的錯誤類型類列表來涵蓋大多數(shù)情況,但如果這些錯誤類型不能滿足要求,還可以創(chuàng)建新的錯誤類型。這種靈活性的基礎(chǔ)在于 JavaScript 允許使用 throw 命令拋出任何內(nèi)容。 可以通過擴展 Error 類以創(chuàng)建自定義錯誤類: class ValidationError extends Error { 可以通過以下方式使用它:
可以使用 try { 3. 拋出錯誤很多人認為錯誤和異常是一回事。實際上,Error 對象只有在被拋出時才會成為異常。 在 JavaScript 中拋出異常,可以使用 throw 來拋出 Error 對象:
或者: throw new TypeError('Expected number'); 來看一個簡單的例子:
在這里,我們檢查函數(shù)參數(shù)是否為字符串。如果不是,就拋出異常。 從技術(shù)上講,我們可以在 JavaScript 中拋出任何東西,而不僅僅是 Error 對象: throw Symbol(); 但是,最好避免這樣做:要拋出正確的 Error 對象,而不是原語。 4. 拋出異常時會發(fā)生什么?異常一旦拋出,就會在程序堆棧中冒泡,除非在某個地方被捕獲。 來看下面的例子:
在瀏覽器或 Node.js 中運行此代碼,程序?qū)⑼V共伋鲥e誤:這里還顯示了發(fā)生錯誤的確切行。這個錯誤就是一個堆棧跟蹤,有助于跟蹤代碼中的問題。堆棧跟蹤從下到上: at toUppercase (<anonymous>:3:11) toUppercase 函數(shù)在第 9 行調(diào)用,在第 3 行拋出錯誤。除了在瀏覽器的控制臺中查看此堆棧跟蹤之外,還可以在 Error 對象的 介紹完這些關(guān)于錯誤的基礎(chǔ)知識之后,下面來看看同步和異步 JavaScript 代碼中的錯誤和異常處理。 5. 同步錯誤處理(1)常規(guī)函數(shù)的錯誤處理同步代碼會按照代碼編寫順序執(zhí)行。讓我們再看看前面的例子:
在這里,引擎調(diào)用并執(zhí)行 toUppercase,這一切都是同步發(fā)生的。 要捕獲由此類同步函數(shù)引發(fā)的異常,可以使用 try/catch/finally: try { 通常,try 會處理正常的路徑,或者可能進行的函數(shù)調(diào)用。catch 就會捕獲實際的異常,它接收 Error 對象。而不管函數(shù)的結(jié)果如何,finally 語句都會運行:無論它失敗還是成功,finally 中的代碼都會運行。 (2)生成器函數(shù)的錯誤處理JavaScript 中的生成器函數(shù)是一種特殊類型的函數(shù)。它可以隨意暫停和恢復(fù),除了在其內(nèi)部范圍和消費者之間提供雙向通信通道。為了創(chuàng)建一個生成器函數(shù),需要在 function 關(guān)鍵字后面加上一個
只要進入函數(shù),就可以使用 yield 來返回值: function* generate() { 生成器函數(shù)的返回值是一個迭代器對象。要從生成器中提取值,可以使用兩種方法:
以上面的代碼為例,要從生成器中獲取值,可以這樣做:
當(dāng)我們調(diào)用生成器函數(shù)時,這里的 go 就是生成的迭代器對象。接下來,就可以調(diào)用 go.next() 來繼續(xù)執(zhí)行: function* generate() { 生成器也可以接受來自調(diào)用者的值和異常。除了 next(),從生成器返回的迭代器對象還有一個 throw() 方法。使用這種方法,就可以通過向生成器中注入異常來停止程序:
要捕獲此類錯誤,可以使用 try/catch 將代碼包裝在生成器中: function* generate() { 生成器函數(shù)也可以向外部拋出異常。 捕獲這些異常的機制與捕獲同步異常的機制相同:try/catch/finally。 下面是使用 for...of 從外部使用的生成器函數(shù)的示例:
輸出結(jié)果如下:這里,try 塊中包含正常的迭代。如果發(fā)生任何異常,就會用 catch 捕獲它。 6. 異步錯誤處理瀏覽器中的異步包括定時器、事件、Promise 等。異步世界中的錯誤處理與同步世界中的處理不同。下面來看一些例子。 (1)定時器的錯誤處理上面我們介紹了如何使用 try/catch/finally 來處理錯誤,那異步中可以使用這些來處理錯誤嗎?先來看一個例子: function failAfterOneSecond() { 此函數(shù)在大約 1 秒后會拋出錯誤。那處理此異常的正確方法是什么?以下代碼是無效的:
我們知道,try/catch是同步的,所以沒辦法這樣來處理異步中的錯誤。當(dāng)傳遞給 setTimeout的回調(diào)運行時,try/catch 早已執(zhí)行完畢。程序?qū)罎ⅲ驗槲茨懿东@異常。它們是在兩條路徑上執(zhí)行的: A: --> try/catch (2)事件的錯誤處理我們可以監(jiān)聽頁面中任何 HTML 元素的事件,DOM 事件的錯誤處理機制遵循與任何異步 Web API 相同的方案。 來看下面的例子:
這里,在單擊按鈕后立即拋出了異常,我們該如何捕獲這個異常呢?這樣寫是不起作用的,也不會阻止程序崩潰: const button = document.querySelector('button'); 與前面的 setTimeout 例子一樣,任何傳遞給 addEventListener 的回調(diào)都是異步執(zhí)行的:
如果不想讓程序崩潰,為了正確處理錯誤,就必須將 try/catch 放到 addEventListener 的回調(diào)中。不過這樣做并不是最佳的處理方式,與 setTimeout 一樣,異步代碼路徑拋出的異常無法從外部捕獲,并且會使程序崩潰。 下面會介紹 Promises 和 async/await 是如何簡化異步代碼的錯誤處理的。 (3)onerrorHTML 元素有許多事件處理程序,例如 來看下面的例子: <body> 當(dāng)訪問的資源缺失時,瀏覽器的控制臺就會報錯:
在 JavaScript 中,可以使用適當(dāng)?shù)氖录幚沓绦颉安东@”此錯誤: const image = document.querySelector('img'); 或者使用 addEventListener 來監(jiān)聽 error 事件,當(dāng)發(fā)生錯誤時進行處理:
此模式對于加載備用資源以代替丟失的圖像或腳本很有用。不過需要記?。簅nerror 與 throw 或 try/catch 是無關(guān)的。 (4)Promise 的錯誤處理下面來通過最上面的 toUppercase 例子看看 Promise 是如何處理錯誤的: function toUppercase(string) { 對上面的代碼進行修改,不返回簡單的字符串或異常,而是分別使用
從技術(shù)上講,這段代碼中沒有任何異步的內(nèi)容,但它可以很好地說明 Promise 的錯誤處理機制。 現(xiàn)在我們就可以在 then 中使用結(jié)果,并使用 catch 來處理被拒絕的 Promise: toUppercase(99) 輸出結(jié)果如下:在 Promise 中,catch 是用來處理錯誤的。除了 catch 還有 finally,類似于 try/catch 中的finally。不管 Promise 結(jié)果如何,finally 都會執(zhí)行:
輸出結(jié)果如下:需要記住,任何傳遞給 then/catch/finally 的回調(diào)都是由微任務(wù)隊列異步處理的。 它們是微任務(wù),優(yōu)先于事件和計時器等宏任務(wù)。 (5)Promise, error, throw作為拒絕 Promise 時的最佳實踐,可以傳入 error 對象: Promise.reject(TypeError('Expected string')); 這樣,在整個代碼庫中保持錯誤處理的一致性。 其他團隊成員總是可以訪問 error.message,更重要的是可以檢查堆棧跟蹤。 除了
這里使用 字符串來 resolve 一個 Promise,然后執(zhí)行鏈立即使用 throw 斷開。為了停止異常的傳播,可以使用 catch 來捕獲錯誤: Promise.resolve('A string') 這種模式在 fetch 中很常見,可以通過檢查 response 對象來查找錯誤:
這里的異??梢允褂?catch 來攔截。 如果失敗了,并且沒有攔截它,異常就會在堆棧中向上冒泡。這本身并沒有什么問題,但不同的環(huán)境對未捕獲的拒絕有不同的反應(yīng)。 例如,Node.js 會讓任何未處理 Promise 拒絕的程序崩潰: DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. 所以,最好去捕獲錯誤。 (6)使用 Promise 處理定時器錯誤對于計時器或事件,不能捕獲回調(diào)拋出的異常。上面有一個例子:
我們可以使用 Promise 來包裝計時器: function failAfterOneSecond() { 這里通過 reject 捕獲了一個 Promise 拒絕,它帶有一個 error 對象。此時就可以用 catch 來處理異常了:
這里使用 value 作為 Promise 的返回值,使用 reason 作為拒絕的返回對象。 (7)Promise.all 的錯誤處理Promise.all 方法接受一個 Promise 數(shù)組,并返回所有解析 Promise 的結(jié)果數(shù)組: const promise1 = Promise.resolve('one'); 如果這些 Promise 中的任何一個被拒絕,Promise.all 將拒絕并返回第一個被拒絕的 Promise 的錯誤。 為了在 Promise.all 中處理這些情況,可以使用 catch:
如果想要運行一個函數(shù)而不考慮 Promise.all 的結(jié)果,可以使用 finally: Promise.all([promise1, promise2, promise3]) (8)Promise.any 的錯誤處理Promise.any 和 Promise.all 恰恰相反。Promise.all 如果某一個失敗,就會拋出第一個失敗的錯誤。而 Promise.any 總是返回第一個成功的 Promise,無論是否發(fā)生任何拒絕。 相反,如果傳遞給 Promise.any 的所有 Promise 都被拒絕,那產(chǎn)生的錯誤就是 AggregateError。 來看下面的例子:
輸出結(jié)果如下:這里用 catch 處理錯誤。AggregateError 對象具有與基本錯誤相同的屬性,外加一個 errors 屬性: const promise1 = Promise.reject(Error('Error')); 此屬性是一個包含所有被拒絕的錯誤信息的數(shù)組: (9)Promise.race 的錯誤處理Promise.race 接受一個 Promise 數(shù)組,并返回第一個成功的 Promise 的結(jié)果:
那如果有被拒絕的 Promise,但它不是傳入數(shù)組中的第一個呢: const promise1 = Promise.resolve('one'); 這樣結(jié)果還是 one,不會影響正常的執(zhí)行。 如果被拒絕的 Promise 是數(shù)組的第一個元素,則 Promise.race 拒絕,就必須要必須捕獲拒絕:
(10)Promise.allSettled 的錯誤處理Promise.allSettled 是 ECMAScript 2020 新增的 API。它和 Promise.all 類似,不過不會被短路,也就是說當(dāng)Promise全部處理完成后,可以拿到每個 Promise 的狀態(tài), 而不管其是否處理成功。 來看下面的例子: const promise1 = Promise.resolve('Good!'); 這里向 Promise.allSettled 傳遞了一個包含兩個 Promise 的數(shù)組:一個已解決,另一個已拒絕。 輸出結(jié)果如下: (11)async/await 的錯誤處理JavaScript 中的 async/await 表示異步函數(shù),用同步的方式去編寫異步,可讀性更好。 下面來改編上面的同步函數(shù) toUppercase,通過將 async 放在 function 關(guān)鍵字之前將其轉(zhuǎn)換為異步函數(shù):
只需在 function 前加上 async 前綴,就可以讓函數(shù)返回一個 Promise。這意味著我們可以在函數(shù)調(diào)用之后鏈式調(diào)用 then、catch 和 finally: toUppercase('hello') 當(dāng)從 async 函數(shù)中拋出異常時,異常會成為底層 Promise 被拒絕的原因。任何錯誤都可以從外部用 catch 攔截。 除此之外,還可以使用 try/catch/finally 來處理錯誤,就像在同步函數(shù)中一樣。 例如,從另一個函數(shù) consumer 中調(diào)用 toUppercase,它方便地用 try/catch/finally 包裝了函數(shù)調(diào)用:
輸出結(jié)果如下: (12)異步生成器的錯誤處理JavaScript 中的異步生成器是能夠生成 Promise 而不是簡單值的生成器函數(shù)。它將生成器函數(shù)與異步相結(jié)合,結(jié)果是一個生成器函數(shù),其迭代器對象向消費者公開一個 Promise。 要創(chuàng)建一個異步生成器,需要聲明一個帶有星號 * 的生成器函數(shù),前綴為 async: async function* asyncGenerator() { 因為異步生成器是基于 Promise,所以同樣適用 Promise 的錯誤處理規(guī)則,在異步生成器中,throw 會導(dǎo)致 Promise 拒絕,可以用 catch 攔截它。 要想從異步生成器處理 Promise,可以使用 then:
輸出結(jié)果如下:也使用異步迭代 for await...of。 要使用異步迭代,需要用 async 函數(shù)包裝 consumer: async function* asyncGenerator() { 與 async/await 一樣,可以使用 try/catch 來處理任何異常:
輸出結(jié)果如下:從異步生成器函數(shù)返回的迭代器對象也有一個 async function* asyncGenerator() { 輸出結(jié)果如下:可以通過以下方式來捕獲錯誤:
我們知道,迭代器對象的 throw() 是在生成器內(nèi)部發(fā)送異常的。所以還可以使用以下方式來處理錯誤: async function* asyncGenerator() { 5. Node.js 錯誤處理(1)同步錯誤處理Node.js 中的同步錯誤處理與 JavaScript 是一樣的,可以使用 try/catch/finally。 (2)異步錯誤處理:回調(diào)模式對于異步代碼,Node.js 強烈依賴兩個術(shù)語:
在回調(diào)模式中,異步 Node.js API 接受一個函數(shù),該函數(shù)通過事件循環(huán)處理并在調(diào)用堆棧為空時立即執(zhí)行。 來看下面的例子:
這里可以看到回調(diào)中錯誤處理: function(error, data) { 如果使用 fs.readFile 讀取給定路徑時出現(xiàn)任何錯誤,我們都會得到一個 error 對象。這時我們可以:
要想拋出異常,可以這樣做:
但是,與 DOM 中的事件和計時器一樣,這個異常會使程序崩潰。 使用 try/catch 停止它的嘗試將不起作用: const { readFile } = require('fs'); 如果不想讓程序崩潰,可以將錯誤傳遞給另一個回調(diào):
這里的 errorHandler 是一個簡單的錯誤處理函數(shù): function errorHandler(error) { (3)異步錯誤處理:事件發(fā)射器Node.js 中的大部分工作都是基于事件的。大多數(shù)時候,我們會與發(fā)射器對象和一些偵聽消息的觀察者進行交互。 Node.js 中的任何事件驅(qū)動模塊(例如 net)都擴展了一個名為 EventEmitter 的根類。EventEmitter 有兩個基本方法:on 和 emit。 下面來看一個簡單的 HTTP 服務(wù)器:
這里我們監(jiān)聽了兩個事件:listening 和 connection。除了這些事件之外,事件發(fā)射器還公開一個錯誤事件,在出現(xiàn)錯誤時觸發(fā)。 如果這段代碼監(jiān)聽的端口是 80,就會得到一個異常: const net = require('net'); 輸出結(jié)果如下:
為了捕獲它,可以為 error 注冊一個事件處理函數(shù): server.on('error', function(error) { 這樣就會輸出:
6. 錯誤處理最佳實踐最后,我們來看看處理 JavaScript 異常的最佳實踐! (1)不要過度處理錯誤錯處理的第一個最佳實踐就是不要過度使用“錯誤處理”。通常,我們會在外層處理錯誤,從內(nèi)層拋出錯誤,這樣一旦出現(xiàn)錯誤,就可以更好地理解是什么原因?qū)е碌摹?/p> 然而,開發(fā)人員常犯的錯誤之一是過度使用錯誤處理。有時這樣做是為了讓代碼在不同的文件和方法中看起來保持一致。但是,不幸的是,這些會對應(yīng)用程序和錯誤檢測造成不利影響。 因此,只關(guān)注代碼中可能導(dǎo)致錯誤的地方,錯誤處理將有助于提高代碼健壯性并增加檢測到錯誤的機會。 (2)避免瀏覽器特定的非標(biāo)準方法盡管許多瀏覽器都遵循一個通用標(biāo)準,但某些特定于瀏覽器的 JavaScript 實現(xiàn)在其他瀏覽器上卻失敗了。例如,以下語法僅適用于 Firefox: catch(e) { 因此,在處理錯誤時,盡可能使用跨瀏覽器友好的 JavaScript 代碼。 (3)遠程錯誤記錄當(dāng)發(fā)生錯誤時,我們應(yīng)該得到通知以了解出了什么問題。這就是錯誤日志的用武之地。JavaScript 代碼是在用戶的瀏覽器中執(zhí)行的。因此,需要一種機制來跟蹤客戶端瀏覽器中的這些錯誤,并將它們發(fā)送到服務(wù)器進行分析。 可以嘗試使用以下工具來監(jiān)控并上報錯誤:
(4)錯誤處理中間件(Node.js)Node.js 環(huán)境支持使用中間件向服務(wù)端應(yīng)用中添加功能。因此可以創(chuàng)建一個錯誤處理中間件。使用中間件的最大好處是所有錯誤都在一個地方集中處理??梢赃x擇啟用/禁用此設(shè)置以輕松進行測試。 以下是創(chuàng)建基本中間件的方法:
可以像下面這樣在應(yīng)用中使用此中間件: const { errorLoggerMiddleware, returnErrorMiddleware } = require('./errorMiddleware') 現(xiàn)在可以在中間件內(nèi)定義自定義邏輯以適當(dāng)?shù)靥幚礤e誤。而無需再擔(dān)心在整個代碼庫中實現(xiàn)單獨的錯誤處理結(jié)構(gòu)。 (5)捕獲所有未捕獲的異常(Node.js)我們可能永遠無法涵蓋應(yīng)用中可能發(fā)生的所有錯誤。因此,必須實施回退策略以捕獲應(yīng)用中所有未捕獲的異常。 可以這樣做:
還可以確定發(fā)生的錯誤是標(biāo)準錯誤還是自定義操作錯誤。根據(jù)結(jié)果,可以退出進程并重新啟動它以避免意外行為。 (6)捕獲所有未處理的 Promise 拒絕(Node.js)與異常不同的是,promise 拒絕不會拋出錯誤。因此,一個被拒絕的 promise 可能只是一個警告,這讓應(yīng)用有可能遇到意外行為。因此,實現(xiàn)處理 promise 拒絕的回退機制至關(guān)重要。 可以這樣做: const promiseRejectionCallback = error => { 參考文章
|
|