Javascript 錯誤處理,"try...catch"

2023-02-17 10:53 更新

不管你多么精通編程,有時我們的腳本總還是會出現(xiàn)錯誤??赡苁且驗槲覀兊木帉懗鲥e,或是與預(yù)期不同的用戶輸入,或是錯誤的服務(wù)端響應(yīng)以及其他數(shù)千種原因。

通常,如果發(fā)生錯誤,腳本就會“死亡”(立即停止),并在控制臺將錯誤打印出來。

但是有一種語法結(jié)構(gòu) try...catch,它使我們可以“捕獲(catch)”錯誤,因此腳本可以執(zhí)行更合理的操作,而不是死掉。

“try…catch” 語法

try...catch 結(jié)構(gòu)由兩部分組成:try 和 catch

try {

  // 代碼...

} catch (err) {

  // 錯誤捕獲

}

它按照以下步驟執(zhí)行:

  1. 首先,執(zhí)行 ?try {...}? 中的代碼。
  2. 如果這里沒有錯誤,則忽略 ?catch (err)?:執(zhí)行到 ?try? 的末尾并跳過 ?catch? 繼續(xù)執(zhí)行。
  3. 如果這里出現(xiàn)錯誤,則 ?try? 執(zhí)行停止,控制流轉(zhuǎn)向 ?catch (err)? 的開頭。變量 ?err?(我們可以使用任何名稱)將包含一個 error 對象,該對象包含了所發(fā)生事件的詳細(xì)信息。


所以,try {...} 塊內(nèi)的 error 不會殺死腳本 —— 我們有機(jī)會在 catch 中處理它。

讓我們來看一些例子。

  • 沒有 error 的例子:顯示 ?alert (1)? 和 ?(2)?:
  • try {
    
      alert('開始執(zhí)行 try 中的內(nèi)容');  // (1) <--
    
      // ...這里沒有 error
    
      alert('try 中的內(nèi)容執(zhí)行完畢');   // (2) <--
    
    } catch (err) {
    
      alert('catch 被忽略,因為沒有 error'); // (3)
    
    }
  • 包含 error 的例子:顯示 (1) 和 (3) 行的 alert 中的內(nèi)容:
  • try {
    
      alert('開始執(zhí)行 try 中的內(nèi)容');  // (1) <--
    
      lalala; // error,變量未定義!
    
      alert('try 的末尾(未執(zhí)行到此處)');  // (2)
    
    } catch (err) {
    
      alert(`出現(xiàn)了 error!`); // (3) <--
    
    }

?try...catch? 僅對運行時的 error 有效

要使得 try...catch 能工作,代碼必須是可執(zhí)行的。換句話說,它必須是有效的 JavaScript 代碼。

如果代碼包含語法錯誤,那么 try..catch 將無法正常工作,例如含有不匹配的花括號:

try {
  {{{{{{{{{{{{
} catch (err) {
  alert("引擎無法理解這段代碼,它是無效的");
}

JavaScript 引擎首先會讀取代碼,然后運行它。在讀取階段發(fā)生的錯誤被稱為“解析時間(parse-time)”錯誤,并且無法恢復(fù)(從該代碼內(nèi)部)。這是因為引擎無法理解該代碼。

所以,try...catch 只能處理有效代碼中出現(xiàn)的錯誤。這類錯誤被稱為“運行時的錯誤(runtime errors)”,有時被稱為“異常(exceptions)”。

?try...catch? 同步執(zhí)行

如果在“計劃的(scheduled)”代碼中發(fā)生異常,例如在 setTimeout 中,則 try...catch 不會捕獲到異常:

try {
  setTimeout(function() {
    noSuchVariable; // 腳本將在這里停止運行
  }, 1000);
} catch (err) {
  alert( "不工作" );
}

因為 try...catch 包裹了計劃要執(zhí)行的函數(shù),該函數(shù)本身要稍后才執(zhí)行,這時引擎已經(jīng)離開了 try...catch 結(jié)構(gòu)。

為了捕獲到計劃的(scheduled)函數(shù)中的異常,那么 try...catch 必須在這個函數(shù)內(nèi):

setTimeout(function() {
  try {
    noSuchVariable; // try...catch 處理 error 了!
  } catch {
    alert( "error 被在這里捕獲了!" );
  }
}, 1000);

Error 對象

發(fā)生錯誤時,JavaScript 會生成一個包含有關(guān)此 error 詳細(xì)信息的對象。然后將該對象作為參數(shù)傳遞給 catch

try {
  // ...
} catch (err) { // <-- “error 對象”,也可以用其他參數(shù)名代替 err
  // ...
}

對于所有內(nèi)建的 error,error 對象具有兩個主要屬性:

?name ?

Error 名稱。例如,對于一個未定義的變量,名稱是 ?"ReferenceError"?。

?message ?

關(guān)于 error 的詳細(xì)文字描述。

還有其他非標(biāo)準(zhǔn)的屬性在大多數(shù)環(huán)境中可用。其中被最廣泛使用和支持的是:

?stack ?

當(dāng)前的調(diào)用棧:用于調(diào)試目的的一個字符串,其中包含有關(guān)導(dǎo)致 error 的嵌套調(diào)用序列的信息。

例如:

try {
  lalala; // error, variable is not defined!
} catch (err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala is not defined
  alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)

  // 也可以將一個 error 作為整體顯示出來
  // error 信息被轉(zhuǎn)換為像 "name: message" 這樣的字符串
  alert(err); // ReferenceError: lalala is not defined
}

可選的 “catch” 綁定

最近新增的特性

這是一個最近添加到 JavaScript 的特性。 舊式瀏覽器可能需要 polyfills.

如果我們不需要 error 的詳細(xì)信息,catch 也可以忽略它:

try {
  // ...
} catch { // <-- 沒有 (err)
  // ...
}

使用 “try…catch”

讓我們一起探究一下真實場景中 try...catch 的用例。

正如我們所知道的,JavaScript 支持 JSON.parse(str) 方法來解析 JSON 編碼的值。

通常,它被用來解析從網(wǎng)絡(luò)、服務(wù)器或是其他來源接收到的數(shù)據(jù)。

我們收到數(shù)據(jù)后,然后像下面這樣調(diào)用 JSON.parse

let json = '{"name":"John", "age": 30}'; // 來自服務(wù)器的數(shù)據(jù)

let user = JSON.parse(json); // 將文本表示轉(zhuǎn)換成 JavaScript 對象

// 現(xiàn)在 user 是一個解析自 json 字符串的有自己屬性的對象
alert( user.name ); // John
alert( user.age );  // 30

你可以在 JSON 方法,toJSON 一章中找到更多關(guān)于 JSON 的詳細(xì)內(nèi)容。

如果 json 格式錯誤,JSON.parse 就會生成一個 error,因此腳本就會“死亡”。

我們對此滿意嗎?當(dāng)然不!

如果這樣做,當(dāng)拿到的數(shù)據(jù)出了問題,那么訪問者永遠(yuǎn)都不會知道原因(除非他們打開開發(fā)者控制臺)。代碼執(zhí)行失敗卻沒有提示信息,這真的是很糟糕的用戶體驗。

讓我們用 try...catch 來處理這個 error:

let json = "{ bad json }";

try {

  let user = JSON.parse(json); // <-- 當(dāng)出現(xiàn) error 時...
  alert( user.name ); // 不工作

} catch (err) {
  // ...執(zhí)行會跳轉(zhuǎn)到這里并繼續(xù)執(zhí)行
  alert( "很抱歉,數(shù)據(jù)有錯誤,我們會嘗試再請求一次。" );
  alert( err.name );
  alert( err.message );
}

在這兒,我們將 catch 塊僅僅用于顯示信息,但我們可以做更多的事:發(fā)送一個新的網(wǎng)絡(luò)請求,向訪問者建議一個替代方案,將有關(guān)錯誤的信息發(fā)送給記錄日志的設(shè)備,……。所有這些都比代碼“死掉”好得多。

拋出我們自定義的 error

如果這個 json 在語法上是正確的,但是沒有所必須的 name 屬性該怎么辦?

像這樣:

let json = '{ "age": 30 }'; // 不完整的數(shù)據(jù)

try {

  let user = JSON.parse(json); // <-- 沒有 error
  alert( user.name ); // 沒有 name!

} catch (err) {
  alert( "doesn't execute" );
}

這里 JSON.parse 正常執(zhí)行,但缺少 name 屬性對我們來說確實是個 error。

為了統(tǒng)一進(jìn)行 error 處理,我們將使用 throw 操作符。

“throw” 操作符

throw 操作符會生成一個 error 對象。

語法如下:

throw <error object>

技術(shù)上講,我們可以將任何東西用作 error 對象。甚至可以是一個原始類型數(shù)據(jù),例如數(shù)字或字符串,但最好使用對象,最好使用具有 name 和 message 屬性的對象(某種程度上保持與內(nèi)建 error 的兼容性)。

JavaScript 中有很多內(nèi)建的標(biāo)準(zhǔn) error 的構(gòu)造器:Error,SyntaxError,ReferenceError,TypeError 等。我們也可以使用它們來創(chuàng)建 error 對象。

它們的語法是:

let error = new Error(message);
// 或
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...

對于內(nèi)建的 error(不是對于其他任何對象,僅僅是對于 error),name 屬性剛好就是構(gòu)造器的名字。message 則來自于參數(shù)(argument)。

例如:

let error = new Error("Things happen o_O");

alert(error.name); // Error
alert(error.message); // Things happen o_O

讓我們來看看 JSON.parse 會生成什么樣的 error:

try {
  JSON.parse("{ bad json o_O }");
} catch(err) {
  alert(err.name); // SyntaxError
  alert(err.message); // Unexpected token b in JSON at position 2
}

正如我們所看到的, 那是一個 SyntaxError。

在我們的示例中,缺少 name 屬性就是一個 error,因為用戶必須有一個 name。

所以,讓我們拋出這個 error。

let json = '{ "age": 30 }'; // 不完整的數(shù)據(jù)

try {

  let user = JSON.parse(json); // <-- 沒有 error

  if (!user.name) {
    throw new SyntaxError("數(shù)據(jù)不全:沒有 name"); // (*)
  }

  alert( user.name );

} catch(err) {
  alert( "JSON Error: " + err.message ); // JSON Error: 數(shù)據(jù)不全:沒有 name
}

在 (*) 標(biāo)記的這一行,throw 操作符生成了包含著我們所給定的 message 的 SyntaxError,與 JavaScript 自己生成的方式相同。try 的執(zhí)行立即停止,控制流轉(zhuǎn)向 catch 塊。

現(xiàn)在,catch 成為了所有 error 處理的唯一場所:對于 JSON.parse 和其他情況都適用。

再次拋出(Rethrowing)

在上面的例子中,我們使用 try...catch 來處理不正確的數(shù)據(jù)。但是在 try {...} 塊中是否可能發(fā)生 另一個預(yù)料之外的 error?例如編程錯誤(未定義變量)或其他錯誤,而不僅僅是這種“不正確的數(shù)據(jù)”。

例如:

let json = '{ "age": 30 }'; // 不完整的數(shù)據(jù)

try {
  user = JSON.parse(json); // <-- 忘記在 user 前放置 "let"

  // ...
} catch (err) {
  alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
  // (實際上并沒有 JSON Error)
}

當(dāng)然,一切皆有可能!程序員也會犯錯。即使是被數(shù)百萬人使用了幾十年的開源項目中,也可能突然被發(fā)現(xiàn)了一個漏洞,并導(dǎo)致可怕的黑客入侵。

在我們的例子中,try...catch 旨在捕獲“數(shù)據(jù)不正確”的 error。但實際上,catch 會捕獲到 所有 來自于 try 的 error。在這兒,它捕獲到了一個預(yù)料之外的 error,但仍然拋出的是同樣的 "JSON Error" 信息。這是不正確的,并且也會使代碼變得更難以調(diào)試。

為了避免此類問題,我們可以采用“重新拋出”技術(shù)。規(guī)則很簡單:

catch 應(yīng)該只處理它知道的 error,并“拋出”所有其他 error。

“再次拋出(rethrowing)”技術(shù)可以被更詳細(xì)地解釋為:

  1. Catch 捕獲所有 error。
  2. 在 ?catch (err) {...}? 塊中,我們對 error 對象 ?err? 進(jìn)行分析。
  3. 如果我們不知道如何處理它,那我們就 ?throw err?。

通常,我們可以使用 ?instanceof? 操作符判斷錯誤類型:

try {
  user = { /*...*/ };
} catch (err) {
  if (err instanceof ReferenceError) {
    alert('ReferenceError'); // 訪問一個未定義(undefined)的變量產(chǎn)生了 "ReferenceError"
  }
}

我們還可以從 err.name 屬性中獲取錯誤的類名。所有原生的錯誤都有這個屬性。另一種方式是讀取 err.constructor.name

在下面的代碼中,我們使用“再次拋出”,以達(dá)到在 catch 中只處理 SyntaxError 的目的:

let json = '{ "age": 30 }'; // 不完整的數(shù)據(jù)
try {

  let user = JSON.parse(json);

  if (!user.name) {
    throw new SyntaxError("數(shù)據(jù)不全:沒有 name");
  }

  blabla(); // 預(yù)料之外的 error

  alert( user.name );

} catch (err) {

  if (err instanceof SyntaxError) {
    alert( "JSON Error: " + err.message );
  } else {
    throw err; // 再次拋出 (*)
  }

}

如果 (*) 標(biāo)記的這行 catch 塊中的 error 從 try...catch 中“掉了出來”,那么它也可以被外部的 try...catch 結(jié)構(gòu)(如果存在)捕獲到,如果外部不存在這種結(jié)構(gòu),那么腳本就會被殺死。

所以,catch 塊實際上只處理它知道該如何處理的 error,并“跳過”所有其他的 error。

下面這個示例演示了這種類型的 error 是如何被另外一級 try...catch 捕獲的:

function readData() {
  let json = '{ "age": 30 }';

  try {
    // ...
    blabla(); // error!
  } catch (err) {
    // ...
    if (!(err instanceof SyntaxError)) {
      throw err; // 再次拋出(不知道如何處理它)
    }
  }
}

try {
  readData();
} catch (err) {
  alert( "External catch got: " + err ); // 捕獲了它!
}

上面這個例子中的 readData 只知道如何處理 SyntaxError,而外部的 try...catch 知道如何處理任意的 error。

try…catch…finally

等一下,以上并不是所有內(nèi)容。

try...catch 結(jié)構(gòu)可能還有一個代碼子句(clause):finally。

如果它存在,它在所有情況下都會被執(zhí)行:

  • ?try? 之后,如果沒有 error,
  • ?catch? 之后,如果有 error。

該擴(kuò)展語法如下所示:

try {
   ... 嘗試執(zhí)行的代碼 ...
} catch (err) {
   ... 處理 error ...
} finally {
   ... 總是會執(zhí)行的代碼 ...
}

試試運行這段代碼:

try {
  alert( 'try' );
  if (confirm('Make an error?')) BAD_CODE();
} catch (err) {
  alert( 'catch' );
} finally {
  alert( 'finally' );
}

這段代碼有兩種執(zhí)行方式:

  1. 如果你對于 “Make an error?” 的回答是 “Yes”,那么執(zhí)行 ?try -> catch -> finally?。
  2. 如果你的回答是 “No”,那么執(zhí)行 ?try -> finally?。

finally 子句(clause)通常用在:當(dāng)我們開始做某事的時候,希望無論出現(xiàn)什么情況都要完成完成某個任務(wù)。

例如,我們想要測量一個斐波那契數(shù)字函數(shù) fib(n) 執(zhí)行所需要花費的時間。通常,我們可以在運行它之前開始測量,并在運行完成時結(jié)束測量。但是,如果在該函數(shù)調(diào)用期間出現(xiàn) error 該怎么辦?特別是,下面這段 fib(n) 的實現(xiàn)代碼在遇到負(fù)數(shù)或非整數(shù)數(shù)字時會返回一個 error。

無論如何,finally 子句都是一個結(jié)束測量的好地方。

在這兒,finally 能夠保證在兩種情況下都能正確地測量時間 —— 成功執(zhí)行 fib 以及 fib 中出現(xiàn) error 時:

let num = +prompt("輸入一個正整數(shù)?", 35)

let diff, result;

function fib(n) {
  if (n < 0 || Math.trunc(n) != n) {
    throw new Error("不能是負(fù)數(shù),并且必須是整數(shù)。");
  }
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}

let start = Date.now();

try {
  result = fib(num);
} catch (err) {
  result = 0;
} finally {
  diff = Date.now() - start;
}

alert(result || "出現(xiàn)了 error");

alert( `執(zhí)行花費了 ${diff}ms` );

你可以通過運行上面這段代碼并在 prompt 彈窗中輸入 35 來進(jìn)行檢查 —— 代碼運行正常,先執(zhí)行 try 然后是 finally。如果你輸入的是 -1 —— 將立即出現(xiàn) error,執(zhí)行將只花費 0ms。以上兩種情況下的時間測量都正確地完成了。

換句話說,函數(shù) fib 以 return 還是 throw 完成都無關(guān)緊要。在這兩種情況下都會執(zhí)行 finally 子句。

變量和 ?try...catch...finally? 中的局部變量

請注意,上面代碼中的 result 和 diff 變量都是在 try...catch 之前 聲明的。

否則,如果我們使用 let 在 try 塊中聲明變量,那么該變量將只在 try 塊中可見。

?finally? 和 ?return?

finally 子句適用于 try...catch 的 任何 出口。這包括顯式的 return

在下面這個例子中,在 try 中有一個 return。在這種情況下,finally 會在控制轉(zhuǎn)向外部代碼前被執(zhí)行。

function func() {

  try {
    return 1;

  } catch (err) {
    /* ... */
  } finally {
    alert( 'finally' );
  }
}

alert( func() ); // 先執(zhí)行 finally 中的 alert,然后執(zhí)行這個 alert

?try...finally?

沒有 catch 子句的 try...finally 結(jié)構(gòu)也很有用。當(dāng)我們不想在這兒處理 error(讓它們 fall through),但是需要確保我們啟動的處理需要被完成。

function func() {
  // 開始執(zhí)行需要被完成的操作(比如測量)
  try {
    // ...
  } finally {
    // 完成前面我們需要完成的那件事,即使 try 中的執(zhí)行失敗了
  }
}

上面的代碼中,由于沒有 catch,所以 try 中的 error 總是會使代碼執(zhí)行跳轉(zhuǎn)至函數(shù) func() 外。但是,在跳出之前需要執(zhí)行 finally 中的代碼。

全局 catch

環(huán)境特定

這個部分的內(nèi)容并不是 JavaScript 核心的一部分。

設(shè)想一下,在 try...catch 結(jié)構(gòu)外有一個致命的 error,然后腳本死亡了。這個 error 就像編程錯誤或其他可怕的事兒那樣。

有什么辦法可以用來應(yīng)對這種情況嗎?我們可能想要記錄這個 error,并向用戶顯示某些內(nèi)容(通常用戶看不到錯誤信息)等。

規(guī)范中沒有相關(guān)內(nèi)容,但是代碼的執(zhí)行環(huán)境一般會提供這種機(jī)制,因為它確實很有用。例如,Node.JS 有 process.on("uncaughtException")。在瀏覽器中,我們可以將一個函數(shù)賦值給特殊的 window.onerror 屬性,該函數(shù)將在發(fā)生未捕獲的 error 時執(zhí)行。

語法如下:

window.onerror = function(message, url, line, col, error) {
  // ...
};

?message ?

error 信息。

?url ?

發(fā)生 error 的腳本的 URL。

?line?,?col ?

發(fā)生 error 處的代碼的行號和列號。

?error ?

error 對象。

例如:

<script>
  window.onerror = function(message, url, line, col, error) {
    alert(`${message}\n At ${line}:${col} of ${url}`);
  };

  function readData() {
    badFunc(); // 啊,出問題了!
  }

  readData();
</script>

全局錯誤處理程序 window.onerror 的作用通常不是恢復(fù)腳本的執(zhí)行 —— 如果發(fā)生編程錯誤,恢復(fù)腳本的執(zhí)行幾乎是不可能的,它的作用是將錯誤信息發(fā)送給開發(fā)者。

也有針對這種情況提供 error 日志的 Web 服務(wù),例如 https://errorception.com 或 http://www.muscula.com。

它們會像這樣運行:

  1. 我們注冊該服務(wù),并拿到一段 JavaScript 代碼(或腳本的 URL),然后插入到頁面中。
  2. 該 JavaScript 腳本設(shè)置了自定義的 ?window.onerror? 函數(shù)。
  3. 當(dāng)發(fā)生 error 時,它會發(fā)送一個此 error 相關(guān)的網(wǎng)絡(luò)請求到服務(wù)提供方。
  4. 我們可以登錄到服務(wù)方的 Web 界面來查看這些 error。

總結(jié)

?try...catch? 結(jié)構(gòu)允許我們處理執(zhí)行過程中出現(xiàn)的 error。從字面上看,它允許“嘗試”運行代碼并“捕獲”其中可能發(fā)生的 error。

語法如下:

try {
  // 執(zhí)行此處代碼
} catch (err) {
  // 如果發(fā)生 error,跳轉(zhuǎn)至此處
  // err 是一個 error 對象
} finally {
  // 無論怎樣都會在 try/catch 之后執(zhí)行
}

這兒可能會沒有 catch 或者沒有 finally,所以 try...catch 或 try...finally 都是可用的。

Error 對象包含下列屬性:

  • ?message? _ 人類可讀的 error 信息。
  • ?name? —— 具有 error 名稱的字符串(Error 構(gòu)造器的名稱)。
  • ?stack?(沒有標(biāo)準(zhǔn),但得到了很好的支持)—— Error 發(fā)生時的調(diào)用棧。

如果我們不需要 error 對象,我們可以通過使用 catch { 而不是 catch (err) { 來省略它。

我們也可以使用 throw 操作符來生成自定義的 error。從技術(shù)上講,throw 的參數(shù)可以是任何東西,但通常是繼承自內(nèi)建的 Error 類的 error 對象。下一章我們會詳細(xì)介紹擴(kuò)展 error。

再次拋出(rethrowing)是一種錯誤處理的重要模式:catch 塊通常期望并知道如何處理特定的 error 類型,因此它應(yīng)該再次拋出它不知道的 error。

即使我們沒有 try...catch,大多數(shù)執(zhí)行環(huán)境也允許我們設(shè)置“全局” error 處理程序來捕獲“掉出(fall out)”的 error。在瀏覽器中,就是 window.onerror。

任務(wù)


使用 finally 還是直接放在代碼后面?

重要程度: 5

比較下面兩個代碼片段。

  1. 第一個代碼片段,使用 finally 在 try..catch 之后執(zhí)行代碼:
  2. try {
      // 工作
    } catch (err) {
      // 處理 error
    } finally {
      // 清理工作空間
    }
  3. 第二個代碼片段,將清空工作空間的代碼放在了 try...catch 之后:
  4. try {
      // 工作
    } catch (err) {
      // 處理 error
    }
    
    // 清理工作空間

我們肯定需要在工作后進(jìn)行清理,無論工作過程中是否有 error 都不影響。

在這兒使用 ?finally? 更有優(yōu)勢,還是說兩個代碼片段效果一樣?如果在這有這樣的優(yōu)勢,如果需要,請舉例說明。


解決方案

當(dāng)我們看函數(shù)中的代碼時,差異就變得很明顯了。

如果在這有“跳出” try..catch 的行為,那么這兩種方式的表現(xiàn)就不同了。

例如,當(dāng) try...catch 中有 return 時。finally 子句會在 try...catch 的 任意 出口處起作用,即使是通過 return 語句退出的也是如此:在 try...catch 剛剛執(zhí)行完成后,但在調(diào)用代碼獲得控制權(quán)之前。

function f() {
  try {
    alert('start');
    return "result";
  } catch (err) {
    /// ...
  } finally {
    alert('cleanup!');
  }
}

f(); // cleanup!

……或者當(dāng)有 throw 時,如下所示:

function f() {
  try {
    alert('start');
    throw new Error("一個 error");
  } catch (err) {
    // ...
    if("無法處理此 error") {
      throw err;
    }

  } finally {
    alert('cleanup!')
  }
}

f(); // cleanup!

正是這里的 finally 保證了 cleanup。如果我們只是將代碼放在函數(shù) f 的末尾,則在這些情況下它不會運行。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號