Javascript錯誤處理——try...catch
Javascript錯誤處理——try…catch
無論我們程式設計多麼精通,指令碼錯誤怎是難免。可能是我們的錯誤造成,或異常輸入,錯誤的伺服器端響應以及無數個其他原因。
通常,當傳送錯誤時指令碼會立刻停止,列印至控制檯。
但
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)
}
alert(“…Then the execution continues”);
帶錯誤示例:顯示
(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 occured!
}
alert(“…Then the execution continues”);
try..catch僅作用在執行時錯誤
對try..catch
,程式碼必須是執行時,換句話說,必須是有效的Javascript程式碼。
如果程式碼語法錯誤不會工作,舉例,下面不會捕獲大括號錯誤:
try {
{{{{{{{{{{{{
} catch(e) {
alert("The engine can't understand this code, it's invalid");
}
Javascript引擎首先讀程式碼,然後執行。發生在讀階段錯誤稱為“解析時”錯誤,不可恢復,因為引擎不理解程式碼。
所以,try...catch
僅能處理有效程式碼中的錯誤,被稱為“執行時”錯誤,有時也稱為“異常”。
try..catch同步執行
如果在預定的(scheduled)程式碼發生異常,如setTimeout
,那麼try...catch
不捕獲異常:
try {
setTimeout(function() {
noSuchVariable; // script will die here
}, 1000);
} catch (e) {
alert( "won't work" );
}
因為try...catch
包裝了setTimeout
呼叫預定函式。但函式會延後執行,此時引擎已經立刻了try...catch
結構。
為了捕獲預定執行函式內的異常,try...catch
必須在函式內部:
setTimeout(function() {
try {
noSuchVariable; // try..catch handles the error!
} catch (e) {
alert( "error is caught here!" );
}
}, 1000);
錯誤物件
當錯誤發生時,Javascript生成包含細節資訊的物件,並作為引數傳遞給catch
塊:
try {
// ...
} catch(err) { // <-- the "error object", could use another word instead of err
// ...
}
對所有內建錯誤,catch
內的錯誤物件主要有兩個屬性:
name
錯誤名稱,一個未定義變數是“ReferenceError”
。
message
錯誤資訊的文字描述。
在大多數環境中有其他非標準屬性,被廣泛使用和支援的一個是:stack
當前呼叫棧:關於導致錯誤的巢狀呼叫序列,用於除錯目的。
舉例:
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 ...
// Can also show an error as a whole
// The error is converted to string as "name: message"
alert(err); // ReferenceError: lalala is not defined
}
使用“try…catch”
讓我們探索一個真實的用例:
我們知道,Javascript支援方法JSON.parse(str)
,用於讀json值。通常用於解析從網路中接收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
非標準的,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 (e) {
// ...the execution jumps here
alert( "Our apologies, the data has errors, we'll try to request it one more time." );
alert( e.name );
alert( e.message );
}
這裡我們使用catch
塊僅顯示資訊,也可以做更多:新的網路請求,建議另一種選擇,傳送錯誤資訊至日誌等,總之都比程式碼直接停止好。
丟擲我們自己的錯誤
如果json語法正在,但沒有需要的name屬性,會怎麼樣?
如下:
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
alert( user.name ); // no name!
} catch (e) {
alert( "doesn't execute" );
}
這裡 JSON.parse
執行正常,但缺少name屬性,實際對我們來說是個錯誤。為了統一錯誤處理,我們需要使用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(e) {
alert(e.name); // SyntaxError
alert(e.message); // Unexpected token o in JSON at position 0
}
如我們所見,錯誤為:SyntaxError
。
在我們的示例中,預設name屬性,也可以視為語法錯誤,假設users必須有個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(e) {
alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
}
在星號行,throw
操作產生SyntaxError
錯誤,並帶有給定的message
,與Javascript自身生成的錯誤一致。try塊中的執行立刻停止,控制流跳至catch
塊。
現在catch
變成了獨立處理所有錯誤的塊:JSON.parse
和其他錯誤。
再次丟擲錯誤
上面的示例,我們使用try...catch
處理不正確的資料,但也可能是其他異常發生在try...catch
塊中,如變數未定義或其他,不僅是“不正確的資料”。
如下:
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
// (not JSON Error actually)
}
當然,一切都是可能的!程式人員造成的錯誤,即使被大量使用的開源工具——可能會突然發現一個瘋狂的bug,導致了可怕的黑客攻擊(就像使用ssh工具發生的那樣)。
在我們的示例中,try...catch
是為了處理不正確資料錯誤,但實際上,catch捕獲try塊中所有的錯誤。如有一個異常錯誤,仍然顯示“JSON Error”訊息,這樣的錯誤也使程式碼更難除錯。
幸運的是,我們能發現捕獲的錯誤是那種型別,示例,從其name屬性:
try {
user = { /*...*/ };
} catch(e) {
alert(e.name); // "ReferenceError" for accessing an undefined variable
}
規則很簡單。
應該僅處理已知錯誤,重新丟擲其他錯誤
詳細的重新丟擲錯誤解釋如下:
- 捕獲所有錯誤
- 在
catch(err){...}
塊中,我們分析錯誤物件err
- 如果不知道如何處理,那麼通過
throw err
丟擲錯誤
下面的程式碼,我們使用重新丟擲錯誤,這樣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(e) {
if (e.name == "SyntaxError") {
alert( "JSON Error: " + e.message );
} else {
throw e; // rethrow (*)
}
}
在catch
塊內部星號行丟擲錯誤,其可以被外部的try...catch
結構塊捕獲(如果存在),或直接停止指令碼。
所以catch
塊實際上僅處理已知錯誤,並忽略所有其他錯誤。
下面示例演示這樣的錯誤被多級try...catch
塊處理。
function readData() {
let json = '{ "age": 30 }';
try {
// ...
blabla(); // error!
} catch (e) {
// ...
if (e.name != 'SyntaxError') {
throw e; // rethrow (don't know how to deal with it)
}
}
}
try {
readData();
} catch (e) {
alert( "External catch got: " + e ); // caught it!
}
這裡readData
僅知道如何處理SyntaxError錯誤,而外部的try...catch
知道如何處理任何錯誤。
try…catch…finally
等等,還沒有完。
結構try...catch
可以有多個程式碼子句:finally
,如果存在,所有情況都會執行。
try
之後,如果沒有錯誤情況catch
之後,如果有錯誤
擴充套件語法類似如下:
try {
... try to execute the code ...
} catch(e) {
... handle errors ...
} finally {
... execute always ...
}
請嘗試執行下面程式碼:
try {
alert( 'try' );
if (confirm('Make an error?')) BAD_CODE();
} catch (e) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
程式碼有兩條執行路徑:
- 如果回答“Yes”產生一個錯誤,那麼執行路徑為
try->catch->finally
. - 如果回撥“No”,那麼路徑為
try->finally
.
子句finally
通常應用場景為:在try...catch
塊之前開始做某事,無論結果如何都需要終止。
舉例,我們想衡量斐波拉切函式fib(n)
執行時間,很自然,我們需要在執行前和結束後衡量。但如果在函式呼叫期間有錯誤?特別是,下面程式碼中的fib(n)的實現將返回一個針對負數或非整數的錯誤。
不管發生什麼,子句finally
很適合去完成時間測量。
finally
負責在兩種場景下測試執行時間——成功執行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 (e) {
result = 0;
} finally {
diff = Date.now() - start;
}
alert(result || "error occured");
alert( `execution took ${diff}ms` );
你可以根據提示輸入35,檢查程式碼執行——執行正常,try之後執行finally。再次輸入-1,立刻產生錯誤,執行花費0ms,正確完成時間測量。
換句話說,有兩種方法可以退出函式:要麼return,要麼丟擲錯誤。finally子控制代碼都會處理。
在try…catch…finally塊中的變數是區域性變數
注意在上面程式碼result
和diff
變數,是在try...catch
塊之前宣告的。
否則,如果使用let在{…}塊裡面,則只能在塊內部可見。
finally 和 return
在try...catch
塊無論如何結束,finally子句都執行。包括顯示的return方式返回。
下面的示例,在try中有return,在這種情況,在控制返回外部程式碼之前,finally被執行。
function func() {
try {
return 1;
} catch (e) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // first works alert from finally, and then this one
try…finally
try…finally結構,沒有catch
子句,也有用。當我們不想在這裡處理錯誤時可以應用,但是要確保開始和最終過程被執行。
function func() {
// start doing something that needs completion (like measurements)
try {
// ...
} finally {
// complete that thing even if all dies
}
}
在上面的程式碼中,try塊的錯誤總會發生,因為沒有catch塊,但finally在執行流跳出外部之前會執行。
全域性錯誤捕獲
環境規範
本節中的資訊不是核心JavaScript的一部分。
我們想像在try...catch
塊之外有個致命錯誤,那麼程式碼會立刻停止。如程式設計錯誤或其他更糟糕的事情。
是否有應對此類事件的方法?我們可能想記錄錯誤,向用戶顯示一些資訊(通常他不會看到錯誤訊息)等等。
Javascript規範中沒有涉及,但環境通常都提供實現,因為確實有用。如,Node.JS有process.on(“uncaughtException”),在瀏覽器中可以給window.onerror賦值一個函式。它將在未捕獲錯誤的情況下執行。
語法:
window.onerror = function(message, url, line, col, error) {
// ...
};
message
錯誤資訊
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.error
的角色通常不能恢復指令碼執行,在程式設計錯誤的情況下是不可能的,但會給開發者傳送錯誤資訊。
工作流程如下:
- 註冊服務,然後獲得一段JS指令碼,插入至頁面中。
- 該JS指令碼中有自定義的
window.error
函式。 - 當錯誤發生時,會給伺服器端傳送網路請求。
- 我們可以登入服務的web介面檢視錯誤資訊。
總結
結構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
——非標準——發生錯誤是的堆疊資訊。
我們也能通過使用throw
操作生成自己的錯誤,技術上,throw
的引數可以是任意型別,但通常是從內建錯誤類Error
繼承的物件。後面會介紹擴充套件錯誤物件。
再次丟擲是錯誤處理的基本模式:catch
塊通常期望並知道怎樣處理特定的錯誤,所以應該重新丟擲未知錯誤。
即使我們沒有使用try...catch
,大多數環境也支援設定全域性的錯誤處理,捕獲所有發生的錯誤,瀏覽器內建的是window.onerror
.