無論我們的程式設計技術有多好,有時我們的腳本還是會有錯誤。它們可能發生在我們犯錯、使用者輸入意外、伺服器回應錯誤,以及其他上千個原因。
通常,腳本會在發生錯誤時「死亡」(立即停止),並將錯誤印出到主控台。
但有一個語法結構 try...catch
允許我們「捕捉」錯誤,因此腳本可以執行更合理的動作,而不是死亡。
「try…catch」語法
try...catch
結構有兩個主要區塊:try
,然後是 catch
try {
// code...
} catch (err) {
// error handling
}
它的運作方式如下
- 首先,執行
try {...}
中的程式碼。 - 如果沒有錯誤,則會忽略
catch (err)
:執行會到達try
的結尾,然後繼續執行,略過catch
。 - 如果發生錯誤,則會停止執行
try
,並將控制權傳遞到catch (err)
的開頭。err
變數(我們可以使用任何名稱)將包含一個錯誤物件,其中包含有關發生情況的詳細資訊。
因此,try {...}
區塊內的錯誤不會終止指令碼 – 我們有機會在 catch
中處理它。
讓我們來看一些範例。
-
無錯誤範例:顯示
alert
(1)
和(2)
try { alert('Start of try runs'); // (1) <-- // ...no errors here alert('End of try runs'); // (2) <-- } catch (err) { alert('Catch is ignored, because there are no errors'); // (3) }
-
有錯誤範例:顯示
(1)
和(3)
try { alert('Start of try runs'); // (1) <-- lalala; // error, variable is not defined! alert('End of try (never reached)'); // (2) } catch (err) { alert(`Error has occurred!`); // (3) <-- }
try...catch
僅適用於執行時期錯誤若要讓 try...catch
正常運作,程式碼必須可執行。換句話說,它應該是有效的 JavaScript。
如果程式碼在語法上錯誤,例如大括號不匹配,則它將無法正常運作
try {
{{{{{{{{{{{{
} catch (err) {
alert("The engine can't understand this code, it's invalid");
}
JavaScript 引擎會先讀取程式碼,然後再執行它。在讀取階段發生的錯誤稱為「解析時期」錯誤,且無法復原(從該程式碼內部)。這是因為引擎無法理解該程式碼。
因此,try...catch
只能處理在有效程式碼中發生的錯誤。此類錯誤稱為「執行時期錯誤」或有時稱為「例外狀況」。
try...catch
同步運作如果例外狀況發生在「排程」程式碼中,例如在 setTimeout
中,則 try...catch
將無法捕捉它
try {
setTimeout(function() {
noSuchVariable; // script will die here
}, 1000);
} catch (err) {
alert( "won't work" );
}
這是因為該函式本身會在稍後執行,而引擎已離開 try...catch
建構。
若要捕捉排程函式內的例外狀況,try...catch
必須在該函式內部
setTimeout(function() {
try {
noSuchVariable; // try...catch handles the error!
} catch {
alert( "error is caught here!" );
}
}, 1000);
錯誤物件
當發生錯誤時,JavaScript 會產生一個包含其詳細資訊的物件。然後將該物件作為引數傳遞給 catch
try {
// ...
} catch (err) { // <-- the "error object", could use another word instead of err
// ...
}
對於所有內建錯誤,錯誤物件有兩個主要屬性
名稱
- 錯誤名稱。例如,對於未定義的變數,為
"ReferenceError"
。 訊息
- 有關錯誤詳細資訊的文字訊息。
在大多數環境中,還有其他非標準屬性可用。最廣泛使用和支援的屬性之一是
堆疊
- 目前的呼叫堆疊:一個字串,其中包含導致錯誤的巢狀呼叫順序資訊。用於除錯目的。
例如
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)
// Can also show an error as a whole
// The error is converted to string as "name: message"
alert(err); // ReferenceError: lalala is not defined
}
選用「catch」繫結
如果我們不需要錯誤詳細資訊,catch
可以省略它
try {
// ...
} catch { // <-- without (err)
// ...
}
使用「try…catch」
讓我們探討 try...catch
的實際使用案例。
我們已經知道 JavaScript 支援 JSON.parse(str) 方法來讀取 JSON 編碼值。
通常用於解碼從網路、伺服器或其他來源接收的資料。
我們接收它並像這樣呼叫 JSON.parse
let json = '{"name":"John", "age": 30}'; // data from the server
let user = JSON.parse(json); // convert the text representation to JS object
// now user is an object with properties from the string
alert( user.name ); // John
alert( user.age ); // 30
您可以在 JSON 方法、toJSON 章節中找到有關 JSON 的更詳細資訊。
如果 json
格式錯誤,JSON.parse
會產生錯誤,因此指令碼會「掛掉」。
我們應該滿足於此嗎?當然不!
這樣一來,如果資料有問題,訪客將永遠不知道(除非他們開啟開發人員主控台)。而人們真的不喜歡在沒有任何錯誤訊息的情況下「掛掉」。
讓我們使用 try...catch
來處理錯誤
let json = "{ bad json }";
try {
let user = JSON.parse(json); // <-- when an error occurs...
alert( user.name ); // doesn't work
} catch (err) {
// ...the execution jumps here
alert( "Our apologies, the data has errors, we'll try to request it one more time." );
alert( err.name );
alert( err.message );
}
這裡我們只使用 catch
區塊來顯示訊息,但我們可以做更多事:發送新的網路要求、向訪客建議替代方案、將錯誤資訊傳送給記錄工具,… 。所有這些都比直接掛掉好得多。
拋出我們自己的錯誤
如果 json
語法正確,但沒有必要的 name
屬性,會怎樣?
像這樣
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
alert( user.name ); // no name!
} catch (err) {
alert( "doesn't execute" );
}
這裡 JSON.parse
正常執行,但對我們來說,沒有 name
實際上是一個錯誤。
為了統一錯誤處理,我們將使用 throw
算子。
「Throw」算子
throw
算子會產生錯誤。
語法為
throw <error object>
技術上來說,我們可以使用任何東西作為錯誤物件。那甚至可以是一個基本型別,例如數字或字串,但最好使用物件,最好是具有 name
和 message
屬性(以與內建錯誤保持一些相容性)。
JavaScript 有許多用於標準錯誤的內建建構函式:Error
、SyntaxError
、ReferenceError
、TypeError
等。我們也可以使用它們來建立錯誤物件。
它們的語法為
let error = new Error(message);
// or
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...
對於內建錯誤(不適用於任何物件,僅適用於錯誤),name
屬性正是建構函式的名稱。而 message
則來自引數。
例如
let error = new Error("Things happen o_O");
alert(error.name); // Error
alert(error.message); // Things happen o_O
讓我們看看 JSON.parse
會產生什麼類型的錯誤
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
的不存在是一個錯誤,因為使用者必須要有 name
。
所以我們來拋出它
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
if (!user.name) {
throw new SyntaxError("Incomplete data: no name"); // (*)
}
alert( user.name );
} catch (err) {
alert( "JSON Error: " + err.message ); // JSON Error: Incomplete data: no name
}
在第 (*)
行,throw
算子會產生一個 SyntaxError
,訊息為給定的 message
,就像 JavaScript 本身會產生它一樣。try
的執行會立即停止,而控制流程會跳到 catch
。
現在 catch
變成所有錯誤處理的單一位置:JSON.parse
和其他案例都是如此。
重新拋出
在上面的範例中,我們使用 try...catch
來處理不正確的資料。但在 try {...}
區塊中,有可能會發生另一個意外的錯誤嗎?例如程式設計錯誤(變數未定義)或其他事情,而不仅仅是這個「不正確的資料」的事情。
例如
let json = '{ "age": 30 }'; // incomplete data
try {
user = JSON.parse(json); // <-- forgot to put "let" before user
// ...
} catch (err) {
alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
// (no JSON Error actually)
}
當然,一切皆有可能!程式設計師會犯錯。即使在數百萬人使用數十年之久的開源工具中,也可能會突然發現一個導致可怕的駭客攻擊的錯誤。
在我們的案例中,try...catch
被放置在捕捉「不正確的資料」錯誤。但根據其本質,catch
會從 try
取得所有錯誤。這裡它取得一個意外的錯誤,但仍然顯示相同的 "JSON Error"
訊息。這是錯誤的,而且也讓程式碼更難除錯。
為了避免此類問題,我們可以使用「重新拋出」技巧。規則很簡單
捕捉只能處理它知道的錯誤,並「重新拋出」所有其他錯誤。
「重新拋出」技巧可以更詳細地說明為
- 捕捉取得所有錯誤。
- 在
catch (err) {...}
區塊中,我們分析錯誤物件err
。 - 如果我們不知道如何處理它,我們會執行
throw err
。
通常,我們可以使用 instanceof
算子來檢查錯誤類型
try {
user = { /*...*/ };
} catch (err) {
if (err instanceof ReferenceError) {
alert('ReferenceError'); // "ReferenceError" for accessing an undefined variable
}
}
我們也可以從 err.name
屬性取得錯誤類別名稱。所有原生錯誤都有它。另一個選項是讀取 err.constructor.name
。
在下面的程式碼中,我們使用重新拋出,讓 catch
只處理 SyntaxError
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json);
if (!user.name) {
throw new SyntaxError("Incomplete data: no name");
}
blabla(); // unexpected error
alert( user.name );
} catch (err) {
if (err instanceof SyntaxError) {
alert( "JSON Error: " + err.message );
} else {
throw err; // rethrow (*)
}
}
在 catch
區塊內第 (*)
行拋出的錯誤會「從」try...catch
中「掉出來」,並且可以被外部的 try...catch
結構(如果存在)捕捉,或是終止指令碼。
因此,catch
區塊實際上只處理它知道如何處理的錯誤,並「跳過」所有其他錯誤。
下面的範例展示此類錯誤如何被另一層的 try...catch
捕捉
function readData() {
let json = '{ "age": 30 }';
try {
// ...
blabla(); // error!
} catch (err) {
// ...
if (!(err instanceof SyntaxError)) {
throw err; // rethrow (don't know how to deal with it)
}
}
}
try {
readData();
} catch (err) {
alert( "External catch got: " + err ); // caught it!
}
這裡 readData
只知道如何處理 SyntaxError
,而外部的 try...catch
知道如何處理所有事情。
try…catch…finally
等等,這還不是全部。
try...catch
結構可能有多一個程式碼子句:finally
。
如果存在,它會在所有情況下執行
- 在
try
之後,如果沒有錯誤, - 在
catch
之後,如果出現錯誤。
延伸的語法看起來像這樣
try {
... try to execute the code ...
} catch (err) {
... handle errors ...
} finally {
... execute always ...
}
試試執行這段程式碼
try {
alert( 'try' );
if (confirm('Make an error?')) BAD_CODE();
} catch (err) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
這段程式碼有兩種執行方式
- 如果你在「產生錯誤?」回答「是」,則為
try -> catch -> finally
。 - 如果你說「否」,則為
try -> finally
。
我們在開始執行某項操作並希望在任何結果狀況下完成它時,通常會使用 finally
子句。
例如,我們想要測量費氏數列函式 fib(n)
執行的時間。自然地,我們可以在它執行之前開始測量,並在執行之後結束測量。但是,如果在函式呼叫期間發生錯誤怎麼辦?特別是,以下程式碼中 fib(n)
的實作會對負數或非整數傳回錯誤。
無論如何,finally
子句都是完成測量的好地方。
這裡的 finally
保證在兩種情況下都能正確測量時間,也就是在 fib
成功執行的情況以及在 fib
發生錯誤的情況
let num = +prompt("Enter a positive integer number?", 35)
let diff, result;
function fib(n) {
if (n < 0 || Math.trunc(n) != n) {
throw new Error("Must not be negative, and also an integer.");
}
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 || "error occurred");
alert( `execution took ${diff}ms` );
你可以透過在 prompt
中輸入 35
來執行程式碼進行檢查,它會正常執行,在 try
之後執行 finally
。然後輸入 -1
,會立即發生錯誤,執行時間會花費 0ms
。兩個測量都正確完成。
換句話說,函式可能會以 return
或 throw
結束,這並不重要。finally
子句在這兩種情況下都會執行。
try...catch...finally
內部是區域性的請注意,上面程式碼中的 result
和 diff
變數是在 try...catch
之前 宣告的。
否則,如果我們在 try
區塊中宣告 let
,它只會在區塊內部可見。
finally
和 return
finally
子句適用於從 try...catch
任何 離開的方式。這包括明確的 return
。
在以下範例中,try
中有一個 return
。在這種情況下,finally
會在控制權傳回外部程式碼之前執行。
function func() {
try {
return 1;
} catch (err) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // first works alert from finally, and then this one
try...finally
沒有 catch
子句的 try...finally
結構也很有用。當我們不想在此處理錯誤(讓它們穿透)時,但希望確保我們啟動的程序會完成時,我們會套用它。
function func() {
// start doing something that needs completion (like measurements)
try {
// ...
} finally {
// complete that thing even if all dies
}
}
在上面的程式碼中,try
內部的錯誤總是會穿透,因為沒有 catch
。但是,finally
會在執行流程離開函式之前執行。
全域性 catch
本節中的資訊不屬於核心 JavaScript 的一部分。
讓我們想像一下,我們在 try...catch
之外發生了致命錯誤,而且指令碼掛掉了。就像程式設計錯誤或其他可怕的事情。
有沒有辦法對此類事件做出反應?我們可能想要記錄錯誤、向使用者顯示一些內容(通常他們看不到錯誤訊息)等。
規格中沒有,但環境通常會提供,因為它真的很有用。例如,Node.js 有 process.on("uncaughtException")
來處理。在瀏覽器中,我們可以將函式指定給特殊 window.onerror 屬性,它會在發生未捕捉錯誤時執行。
語法
window.onerror = function(message, url, line, col, error) {
// ...
};
訊息
- 錯誤訊息。
url
- 發生錯誤的腳本 URL。
line
、col
- 發生錯誤的行和欄位號碼。
error
- 錯誤物件。
例如
<script>
window.onerror = function(message, url, line, col, error) {
alert(`${message}\n At ${line}:${col} of ${url}`);
};
function readData() {
badFunc(); // Whoops, something went wrong!
}
readData();
</script>
全域處理函式 window.onerror
的角色通常不是要復原腳本執行,因為在發生程式設計錯誤時,這可能是無法做到的,而是將錯誤訊息傳送給開發人員。
也有提供此類錯誤記錄的網路服務,例如 https://errorception.com 或 https://www.muscula.com。
它們的工作方式如下
- 我們在服務中註冊,並從中取得一段 JS(或腳本 URL)插入到網頁中。
- 該 JS 腳本會設定自訂
window.onerror
函式。 - 當發生錯誤時,它會傳送網路要求給服務。
- 我們可以登入服務網路介面查看錯誤。
摘要
try...catch
建構式允許處理執行時期錯誤。它實際上允許「嘗試」執行程式碼,並「捕捉」可能在其中發生的錯誤。
語法為
try {
// run this code
} catch (err) {
// if an error happened, then jump here
// err is the error object
} finally {
// do in any case after try/catch
}
可能沒有 catch
區段或 finally
,因此較簡短的建構式 try...catch
和 try...finally
也是有效的。
錯誤物件具有下列屬性
message
– 人類可讀的錯誤訊息。name
– 含有錯誤名稱(錯誤建構函式名稱)的字串。stack
(非標準,但支援良好) – 錯誤建立時的堆疊。
如果不需要錯誤物件,我們可以使用 catch {
代替 catch (err) {
來省略它。
我們也可以使用 throw
算子產生自己的錯誤。技術上來說,throw
的引數可以是任何東西,但通常是繼承自內建 Error
類別的錯誤物件。有關在下一章節中延伸錯誤的更多資訊。
重新擲回 是錯誤處理中非常重要的模式:catch
區塊通常會預期並知道如何處理特定錯誤類型,因此它應該重新擲回它不認識的錯誤。
即使我們沒有 try...catch
,大多數環境允許我們設定一個「全域」錯誤處理常式來捕捉「掉出來」的錯誤。在瀏覽器中,那就是 window.onerror
。
留言
<code>
標籤,對於多行,請將它們包覆在<pre>
標籤中,對於超過 10 行,請使用沙盒 (plnkr、jsbin、codepen…)