大家好,我是 Echa。
本文將帶你了解 JAVAscript 中常見的錯誤類型,處理同步和異步 JavaScript/Node.js 代碼中錯誤和異常的方式,以及錯誤處理最佳實踐!
1. 錯誤概述
JavaScript 中的錯誤是一個對象,在發生錯誤時會拋出該對象以停止程序。在 JavaScript 中,可以通過構造函數來創建一個新的通用錯誤:
const err = new Error("Error");
當然,也可以省略 new 關鍵字:
const err = Error("Error");
Error 對象有三個屬性:
- message:帶有錯誤消息的字符串;
- name: 錯誤的類型;
- stack:函數執行的堆棧跟蹤。
例如,創建一個 TypeError 對象,該消息將攜帶實際的錯誤字符串,其 name 將是“TypeError”:
const wrongType = TypeError("Expected number");wrongType.message; // 'Expected number'wrongType.name; // 'TypeError'
堆棧跟蹤是發生異常或警告等事件時程序所處的方法調用列表:
它首先會打印錯誤名稱和消息,然后是被調用的方法列表。每個方法調用都說明其源代碼的位置和調用它的行。可以使用此數據來瀏覽代碼庫并確定導致錯誤的代碼段。此方法列表以堆疊的方式排列。它顯示了異常首先被拋出的位置以及它如何通過堆棧方法調用傳播。為異常實施捕獲不會讓它通過堆棧向上傳播并使程序崩潰。
對于 Error 對象,Firefox 還實現了一些非標準屬性:
- columnNumber:錯誤所在行的列號;
- filename:發生錯誤的文件
- lineNumber:發生錯誤的行號
2. 錯誤類型
JavaScript 中有一系列預定義的錯誤類型。只要使用者沒有明確處理應用程序中的錯誤,它們就會由 JavaScript 運行時自動選擇和定義。
JavaScript中的錯誤類型包括:
- EvalError
- InternalError
- RangeError
- ReferenceError
- SyntaxError
- TypeError
- URIError
這些錯誤類型都是實際的構造函數,旨在返回一個新的錯誤對象。最常見的就是 TypeError。大多數時候,大部分錯誤將直接來自 JavaScript 引擎,例如 InternalError 或 SyntaxError。
JavaScript 提供了 instanceof 運算符可以用于區分異常類型:
try {If (typeof x !== ‘number’) {throw new TypeError(‘x 應是數字’);} else if (x <= 0) {throw new RangeError('x 應大于 0');} else {} catch (err) {if (err instanceof TypeError) {// 處理 TypeError 錯誤} else if (err instanceof RangeError) {// 處理 RangeError 錯誤} else {// 處理其他類型錯誤
下面來了解 JavaScript 中最常見的錯誤類型,并了解它們發生的時間和原因。
(1)SyntaxError
SyntaxError 表示語法錯誤。這些錯誤是最容易修復的錯誤之一,因為它們表明代碼語法中存在錯誤。由于 JavaScript 是一種解釋而非編譯的腳本語言,因此當應用程序執行包含錯誤的腳本時會拋出這些錯誤。在編譯語言的情況下,此類錯誤在編譯期間被識別。因此,在修復這些問題之前,不會創建應用程序二進制文件。
SyntaxError 發生的一些常見原因是:
- 缺少引號
- 缺少右括號
- 大括號或其他字符對齊不當
(2)TypeError
TypeError 是 JavaScript 應用程序中最常見的錯誤之一,當某些值不是特定的預期類型時,就會產生此錯誤。
TypeError 發生的一些常見原因是:
- 調用不是方法的對象。
- 試圖訪問 null 或未定義對象的屬性
- 將字符串視為數字,反之亦然
ReferenceError 表示引用錯誤。當代碼中的變量引用有問題時,會發生 ReferenceError。可能忘記在使用變量之前為其定義一個值,或者可能試圖在代碼中使用一個不可訪問的變量。在任何情況下,通過堆棧跟蹤都可以提供充足的信息來查找和修復有問題的變量引用。
ReferenceErrors 發生的一些常見原因如下:
- 在變量名中輸入錯誤。
- 試圖訪問其作用域之外的塊作用域變量。
- 在加載之前從外部庫引用全局變量。
(4)RangeError
RangeError 表示范圍錯誤。當變量設置的值超出其合法值范圍時,將拋出 RangeError。它通常發生在將值作為參數傳遞給函數時,并且給定值不在函數參數的范圍內。當使用記錄不完整的第三方庫時,有時修復起來會很棘手,因為需要知道參數的可能值范圍才能傳遞正確的值。
RangeError 發生的一些常見場景如下:
- 試圖通過 Array 構造函數創建非法長度的數組。
- 將錯誤的值傳遞給數字方法,例如 toExponential()、toPrecision()、toFixed()等。
- 將非法值傳遞給字符串函數,例如 normalize()。
(5)URIError
URIError 表示 URI錯誤。當 URI 的編碼和解碼出現問題時,會拋出 URIError。JavaScript 中的 URI 操作函數包括:decodeURI、decodeURIComponent 等。如果使用了錯誤的參數(無效字符),就會拋出 URIError。
(6)EvalError
EvalError 表示 Eval 錯誤。當 eval() 函數調用發生錯誤時,會拋出 EvalError。不過,當前的 JavaScript 引擎或 ECMAScript 規范不再拋出此錯誤。但是,為了向后兼容,它仍然是存在的。
如果使用的是舊版本的 JavaScript,可能會遇到此錯誤。在任何情況下,最好調查在eval()函數調用中執行的代碼是否有任何異常。
(7)InternalError
InternalError 表示內部錯誤。在 JavaScript 運行時引擎發生異常時使用。它表示代碼可能存在問題也可能不存在問題。
InternalError 通常只發生在兩種情況下:
- 當 JavaScript 運行時的補丁或更新帶有引發異常的錯誤時(這種情況很少發生);
- 當代碼包含對于 JavaScript 引擎而言太大的實體時(例如,數組初始值設定項太大、遞歸太多)。
解決此錯誤最合適的方法就是通過錯誤消息確定原因,并在可能的情況下重構應用邏輯,以消除 JavaScript 引擎上工作負載的突然激增。
注意:現代 JavaScript 中不會拋出 EvalError 和 InternalError。
(8)創建自定義錯誤類型
雖然 JavaScript 提供了足夠的錯誤類型類列表來涵蓋大多數情況,但如果這些錯誤類型不能滿足要求,還可以創建新的錯誤類型。這種靈活性的基礎在于 JavaScript 允許使用 throw 命令拋出任何內容。
可以通過擴展 Error 類以創建自定義錯誤類:
class Validationerror extends Error {constructor(message) {super(message);this.name = "ValidationError";
可以通過以下方式使用它:
throw ValidationError("未找到該屬性: name")
可以使用 instanceof 關鍵字識別它:
try {validateForm() // 拋出 ValidationError 的代碼} catch (e) {if (e instanceof ValidationError) {else {
3. 拋出錯誤
很多人認為錯誤和異常是一回事。實際上,Error 對象只有在被拋出時才會成為異常。
在 JavaScript 中拋出異常,可以使用 throw 來拋出 Error 對象:
throw TypeError("Expected number");
或者:
throw new TypeError("Expected number");
來看一個簡單的例子:
function toUppercase(string) {if (typeof string !== "string") {throw TypeError("Expected string");return string.toUpperCase();
在這里,我們檢查函數參數是否為字符串。如果不是,就拋出異常。
從技術上講,我們可以在 JavaScript 中拋出任何東西,而不僅僅是 Error 對象:
throw Symbol();throw 33;throw "Error!";throw null;
但是,最好避免這樣做:要拋出正確的 Error 對象,而不是原語。
4. 拋出異常時會發生什么?
異常一旦拋出,就會在程序堆棧中冒泡,除非在某個地方被捕獲。
來看下面的例子:
function toUppercase(string) {if (typeof string !== "string") {throw TypeError("Expected string");return string.toUpperCase();toUppercase(4);
在瀏覽器或 Node.js 中運行此代碼,程序將停止并拋出錯誤:
這里還顯示了發生錯誤的確切行。這個錯誤就是一個堆棧跟蹤,有助于跟蹤代碼中的問題。堆棧跟蹤從下到上:
at toUppercase (:3:11)at :9:1
toUppercase 函數在第 9 行調用,在第 3 行拋出錯誤。除了在瀏覽器的控制臺中查看此堆棧跟蹤之外,還可以在 Error 對象的 stack 屬性上訪問它。
介紹完這些關于錯誤的基礎知識之后,下面來看看同步和異步 JavaScript 代碼中的錯誤和異常處理。
5. 同步錯誤處理(1)常規函數的錯誤處理
同步代碼會按照代碼編寫順序執行。讓我們再看看前面的例子:
function toUppercase(string) {if (typeof string !== "string") {throw TypeError("Expected string");return string.toUpperCase();toUppercase(4);
在這里,引擎調用并執行 toUppercase,這一切都是同步發生的。 要捕獲由此類同步函數引發的異常,可以使用 try/catch/finally:
try {toUppercase(4);} catch (error) {console.error(error.message);} finally {
通常,try 會處理正常的路徑,或者可能進行的函數調用。catch 就會捕獲實際的異常,它接收 Error 對象。而不管函數的結果如何,finally 語句都會運行:無論它失敗還是成功,finally 中的代碼都會運行。
(2)生成器函數的錯誤處理
JavaScript 中的生成器函數是一種特殊類型的函數。它可以隨意暫停和恢復,除了在其內部范圍和消費者之間提供雙向通信通道。為了創建一個生成器函數,需要在 function 關鍵字后面加上一個 *:
function* generate() {
只要進入函數,就可以使用 yield 來返回值:
function* generate() {yield 33;yield 99;
生成器函數的返回值是一個迭代器對象。要從生成器中提取值,可以使用兩種方法:
- 在迭代器對象上調用 next()
- 使用 for...of 進行迭代
以上面的代碼為例,要從生成器中獲取值,可以這樣做:
function* generate() {yield 33;yield 99;const go = generate();
當我們調用生成器函數時,這里的 go 就是生成的迭代器對象。接下來,就可以調用 go.next() 來繼續執行:
function* generate() {yield 33;yield 99;const go = generate();const firstStep = go.next().value; // 33const secondStep = go.next().value; // 99
生成器也可以接受來自調用者的值和異常。除了 next(),從生成器返回的迭代器對象還有一個 throw() 方法。使用這種方法,就可以通過向生成器中注入異常來停止程序:
function* generate() {yield 33;yield 99;const go = generate();const firstStep = go.next().value; // 33go.throw(Error("Tired of iterating!"));const secondStep = go.next().value; // never reached
要捕獲此類錯誤,可以使用 try/catch 將代碼包裝在生成器中:
function* generate() {try {yield 33;yield 99;} catch (error) {console.error(error.message);
生成器函數也可以向外部拋出異常。 捕獲這些異常的機制與捕獲同步異常的機制相同:try/catch/finally。
下面是使用 for...of 從外部使用的生成器函數的示例:
function* generate() {yield 33;yield 99;throw Error("Tired of iterating!");try {for (const value of generate()) {console.log(value);} catch (error) {console.error(error.message);
輸出結果如下:
這里,try 塊中包含正常的迭代。如果發生任何異常,就會用 catch 捕獲它。
6. 異步錯誤處理
瀏覽器中的異步包括定時器、事件、Promise 等。異步世界中的錯誤處理與同步世界中的處理不同。下面來看一些例子。
(1)定時器的錯誤處理
上面我們介紹了如何使用 try/catch/finally 來處理錯誤,那異步中可以使用這些來處理錯誤嗎?先來看一個例子:
function failAfterOneSecond() {setTimeout(() => {throw Error("Wrong!");}, 1000);
此函數在大約 1 秒后會拋出錯誤。那處理此異常的正確方法是什么?以下代碼是無效的:
function failAfterOneSecond() {setTimeout(() => {throw Error("Wrong!");}, 1000);try {failAfterOneSecond();} catch (error) {console.error(error.message);
我們知道,try/catch是同步的,所以沒辦法這樣來處理異步中的錯誤。當傳遞給 setTimeout的回調運行時,try/catch 早已執行完畢。程序將會崩潰,因為未能捕獲異常。它們是在兩條路徑上執行的:
A: --> try/catchB: --> setTimeout --> callback --> throw
(2)事件的錯誤處理
我們可以監聽頁面中任何 html 元素的事件,DOM 事件的錯誤處理機制遵循與任何異步 Web API 相同的方案。
來看下面的例子:
const button = document.querySelector("button");button.addEventListener("click", function() {throw Error("error");
這里,在單擊按鈕后立即拋出了異常,我們該如何捕獲這個異常呢?這樣寫是不起作用的,也不會阻止程序崩潰:
const button = document.querySelector("button");try {button.addEventListener("click", function() {throw Error("error");} catch (error) {console.error(error.message);
與前面的 setTimeout 例子一樣,任何傳遞給 addEventListener 的回調都是異步執行的:
Track A: --> try/catchTrack B: --> addEventListener --> callback --> throw
如果不想讓程序崩潰,為了正確處理錯誤,就必須將 try/catch 放到 addEventListener 的回調中。不過這樣做并不是最佳的處理方式,與 setTimeout 一樣,異步代碼路徑拋出的異常無法從外部捕獲,并且會使程序崩潰。
下面會介紹 Promises 和 async/await 是如何簡化異步代碼的錯誤處理的。
(3)onerror
HTML 元素有許多事件處理程序,例如 onclick、onmouseenter、onchange 等。除此之外,還有 onerror,每當標簽或