不管妳多麽精通編程,有時我們的腳本總還是會出現錯誤。可能是因爲我們的編寫出錯,或是與預期不同的用戶輸入,或是錯誤的服務端響應以及其他數千種原因。
通常,如果發生錯誤,腳本就會“死亡”(立即停止),並在控制台將錯誤打印出來。
但是有壹種語法結構 try...catch
,它使我們可以“捕獲(catch)”錯誤,因此腳本可以執行更合理的操作,而不是死掉。
try...catch
結構由兩部分組成:try
和 catch
:
try { // 代碼... } catch (err) { // 錯誤捕獲 }
它按照以下步驟執行:
首先,執行 try {...}
中的代碼。
如果這裏沒有錯誤,則忽略 catch (err)
:執行到 try
的末尾並跳過 catch
繼續執行。
如果這裏出現錯誤,則 try
執行停止,控制流轉向 catch (err)
的開頭。變量 err
(我們可以使用任何名稱)將包含壹個 error 對象,該對象包含了所發生事件的詳細信息。
所以,try {...}
塊內的 error 不會殺死腳本 —— 我們有機會在 catch
中處理它。
讓我們來看壹些例子。
沒有 error 的例子:顯示 alert
(1)
和 (2)
:
try { alert('開始執行 try 中的內容'); // (1) <-- // ...這裏沒有 error alert('try 中的內容執行完畢'); // (2) <-- } catch (err) { alert('catch 被忽略,因爲沒有 error'); // (3) }
包含 error 的例子:顯示 (1)
和 (3)
行的 alert
中的內容:
try { alert('開始執行 try 中的內容'); // (1) <-- lalala; // error,變量未定義! alert('try 的末尾(未執行到此處)'); // (2) } catch (err) { alert(`出現了 error!`); // (3) <-- }
try...catch
僅對運行時的 error 有效
要使得 try...catch
能工作,代碼必須是可執行的。換句話說,它必須是有效的 JavaScript 代碼。
如果代碼包含語法錯誤,那麽 try..catch
將無法正常工作,例如含有不匹配的花括號:
try { {{{{{{{{{{{{ } catch (err) { alert("引擎無法理解這段代碼,它是無效的"); }
JavaScript 引擎首先會讀取代碼,然後運行它。在讀取階段發生的錯誤被稱爲“解析時間(parse-time)”錯誤,並且無法恢複(從該代碼內部)。這是因爲引擎無法理解該代碼。
所以,try...catch
只能處理有效代碼中出現的錯誤。這類錯誤被稱爲“運行時的錯誤(runtime errors)”,有時被稱爲“異常(exceptions)”。
try...catch
同步執行
如果在“計劃的(scheduled)”代碼中發生異常,例如在 setTimeout
中,則 try...catch
不會捕獲到異常:
try { setTimeout(function() { noSuchVariable; // 腳本將在這裏停止運行 }, 1000); } catch (err) { alert( "不工作" ); }
因爲 try...catch
包裹了計劃要執行的函數,該函數本身要稍後才執行,這時引擎已經離開了 try...catch
結構。
爲了捕獲到計劃的(scheduled)函數中的異常,那麽 try...catch
必須在這個函數內:
setTimeout(function() { try { noSuchVariable; // try...catch 處理 error 了! } catch { alert( "error 被在這裏捕獲了!" ); } }, 1000);
發生錯誤時,JavaScript 會生成壹個包含有關此 error 詳細信息的對象。然後將該對象作爲參數傳遞給 catch
:
try { // ... } catch (err) { // <-- “error 對象”,也可以用其他參數名代替 err // ... }
對于所有內建的 error,error 對象具有兩個主要屬性:
name
Error 名稱。例如,對于壹個未定義的變量,名稱是 "ReferenceError"
。
message
關于 error 的詳細文字描述。
還有其他非標准的屬性在大多數環境中可用。其中被最廣泛使用和支持的是:
stack
當前的調用棧:用于調試目的的壹個字符串,其中包含有關導致 error 的嵌套調用序列的信息。
例如:
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 信息被轉換爲像 "name: message" 這樣的字符串 alert(err); // ReferenceError: lalala is not defined }
最近新增的特性
這是壹個最近添加到 JavaScript 的特性。 舊式浏覽器可能需要 polyfills.
如果我們不需要 error 的詳細信息,catch
也可以忽略它:
try { // ... } catch { // <-- 沒有 (err) // ... }
讓我們壹起探究壹下真實場景中 try...catch
的用例。
正如我們所知道的,JavaScript 支持 JSON.parse(str) 方法來解析 JSON 編碼的值。
通常,它被用來解析從網絡、服務器或是其他來源接收到的數據。
我們收到數據後,然後像下面這樣調用 JSON.parse
:
let json = '{"name":"John", "age": 30}'; // 來自服務器的數據 let user = JSON.parse(json); // 將文本表示轉換成 JavaScript 對象 // 現在 user 是壹個解析自 json 字符串的有自己屬性的對象 alert( user.name ); // John alert( user.age ); // 30
妳可以在 JSON 方法,toJSON 壹章中找到更多關于 JSON 的詳細內容。
如果 json
格式錯誤,JSON.parse
就會生成壹個 error,因此腳本就會“死亡”。
我們對此滿意嗎?當然不!
如果這樣做,當拿到的數據出了問題,那麽訪問者永遠都不會知道原因(除非他們打開開發者控制台)。代碼執行失敗卻沒有提示信息,這真的是很糟糕的用戶體驗。
讓我們用 try...catch
來處理這個 error:
let json = "{ bad json }"; try { let user = JSON.parse(json); // <-- 當出現 error 時... alert( user.name ); // 不工作 } catch (err) { // ...執行會跳轉到這裏並繼續執行 alert( "很抱歉,數據有錯誤,我們會嘗試再請求壹次。" ); alert( err.name ); alert( err.message ); }
在這兒,我們將 catch
塊僅僅用于顯示信息,但我們可以做更多的事:發送壹個新的網絡請求,向訪問者建議壹個替代方案,將有關錯誤的信息發送給記錄日志的設備,……。所有這些都比代碼“死掉”好得多。
如果這個 json
在語法上是正確的,但是沒有所必須的 name
屬性該怎麽辦?
像這樣:
let json = '{ "age": 30 }'; // 不完整的數據 try { let user = JSON.parse(json); // <-- 沒有 error alert( user.name ); // 沒有 name! } catch (err) { alert( "doesn't execute" ); }
這裏 JSON.parse
正常執行,但缺少 name
屬性對我們來說確實是個 error。
爲了統壹進行 error 處理,我們將使用 throw
操作符。
throw
操作符會生成壹個 error 對象。
語法如下:
throw <error object>
技術上講,我們可以將任何東西用作 error 對象。甚至可以是壹個原始類型數據,例如數字或字符串,但最好使用對象,最好使用具有 name
和 message
屬性的對象(某種程度上保持與內建 error 的兼容性)。
JavaScript 中有很多內建的標准 error 的構造器:Error
,SyntaxError
,ReferenceError
,TypeError
等。我們也可以使用它們來創建 error 對象。
它們的語法是:
let error = new Error(message); // 或 let error = new SyntaxError(message); let error = new ReferenceError(message); // ...
對于內建的 error(不是對于其他任何對象,僅僅是對于 error),name
屬性剛好就是構造器的名字。message
則來自于參數(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 }'; // 不完整的數據 try { let user = JSON.parse(json); // <-- 沒有 error if (!user.name) { throw new SyntaxError("數據不全:沒有 name"); // (*) } alert( user.name ); } catch(err) { alert( "JSON Error: " + err.message ); // JSON Error: 數據不全:沒有 name }
在 (*)
標記的這壹行,throw
操作符生成了包含著我們所給定的 message
的 SyntaxError
,與 JavaScript 自己生成的方式相同。try
的執行立即停止,控制流轉向 catch
塊。
現在,catch
成爲了所有 error 處理的唯壹場所:對于 JSON.parse
和其他情況都適用。
在上面的例子中,我們使用 try...catch
來處理不正確的數據。但是在 try {...}
塊中是否可能發生 另壹個預料之外的 error?例如編程錯誤(未定義變量)或其他錯誤,而不僅僅是這種“不正確的數據”。
例如:
let json = '{ "age": 30 }'; // 不完整的數據 try { user = JSON.parse(json); // <-- 忘記在 user 前放置 "let" // ... } catch (err) { alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined // (實際上並沒有 JSON Error) }
當然,壹切皆有可能!程序員也會犯錯。即使是被數百萬人使用了幾十年的開源項目中,也可能突然被發現了壹個漏洞,並導致可怕的黑客入侵。
在我們的例子中,try...catch
旨在捕獲“數據不正確”的 error。但實際上,catch 會捕獲到 所有 來自于 try
的 error。在這兒,它捕獲到了壹個預料之外的 error,但仍然抛出的是同樣的 "JSON Error"
信息。這是不正確的,並且也會使代碼變得更難以調試。
爲了避免此類問題,我們可以采用“重新抛出”技術。規則很簡單:
catch
應該只處理它知道的 error,並“抛出”所有其他 error。
“再次抛出(rethrowing)”技術可以被更詳細地解釋爲:
Catch 捕獲所有 error。
在 catch (err) {...}
塊中,我們對 error 對象 err
進行分析。
如果我們不知道如何處理它,那我們就 throw err
。
通常,我們可以使用 instanceof
操作符判斷錯誤類型:
try { user = { /*...*/ }; } catch (err) { if (err instanceof ReferenceError) { alert('ReferenceError'); // 訪問壹個未定義(undefined)的變量産生了 "ReferenceError" } }
我們還可以從 err.name
屬性中獲取錯誤的類名。所有原生的錯誤都有這個屬性。另壹種方式是讀取 err.constructor.name
。
在下面的代碼中,我們使用“再次抛出”,以達到在 catch
中只處理 SyntaxError
的目的:
let json = '{ "age": 30 }'; // 不完整的數據 try { let user = JSON.parse(json); if (!user.name) { throw new SyntaxError("數據不全:沒有 name"); } blabla(); // 預料之外的 error alert( user.name ); } catch (err) { if (err instanceof SyntaxError) { alert( "JSON Error: " + err.message ); } else { throw err; // 再次抛出 (*) } }
如果 (*)
標記的這行 catch
塊中的 error 從 try...catch
中“掉了出來”,那麽它也可以被外部的 try...catch
結構(如果存在)捕獲到,如果外部不存在這種結構,那麽腳本就會被殺死。
所以,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
結構可能還有壹個代碼子句(clause):finally
。
如果它存在,它在所有情況下都會被執行:
try
之後,如果沒有 error,
catch
之後,如果有 error。
該擴展語法如下所示:
try { ... 嘗試執行的代碼 ... } catch (err) { ... 處理 error ... } finally { ... 總是會執行的代碼 ... }
試試運行這段代碼:
try { alert( 'try' ); if (confirm('Make an error?')) BAD_CODE(); } catch (err) { alert( 'catch' ); } finally { alert( 'finally' ); }
這段代碼有兩種執行方式:
如果妳對于 “Make an error?” 的回答是 “Yes”,那麽執行 try -> catch -> finally
。
如果妳的回答是 “No”,那麽執行 try -> finally
。
finally
子句(clause)通常用在:當我們開始做某事的時候,希望無論出現什麽情況都要完成某個任務。
例如,我們想要測量壹個斐波那契數字函數 fib(n)
執行所需要花費的時間。通常,我們可以在運行它之前開始測量,並在運行完成時結束測量。但是,如果在該函數調用期間出現 error 該怎麽辦?特別是,下面這段 fib(n)
的實現代碼在遇到負數或非整數數字時會返回壹個 error。
無論如何,finally
子句都是壹個結束測量的好地方。
在這兒,finally
能夠保證在兩種情況下都能正確地測量時間 —— 成功執行 fib
以及 fib
中出現 error 時:
let num = +prompt("輸入壹個正整數?", 35) let diff, result; function fib(n) { if (n < 0 || Math.trunc(n) != n) { throw new Error("不能是負數,並且必須是整數。"); } 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"); alert( `執行花費了 ${diff}ms` );
妳可以通過運行上面這段代碼並在 prompt
彈窗中輸入 35
來進行檢查 —— 代碼運行正常,先執行 try
然後是 finally
。如果妳輸入的是 -1
—— 將立即出現 error,執行將只花費 0ms
。以上兩種情況下的時間測量都正確地完成了。
換句話說,函數 fib
以 return
還是 throw
完成都無關緊要。在這兩種情況下都會執行 finally
子句。
變量和 try...catch...finally
中的局部變量
請注意,上面代碼中的 result
和 diff
變量都是在 try...catch
之前 聲明的。
否則,如果我們使用 let
在 try
塊中聲明變量,那麽該變量將只在 try
塊中可見。
finally
和 return
finally
子句適用于 try...catch
的 任何 出口。這包括顯式的 return
。
在下面這個例子中,在 try
中有壹個 return
。在這種情況下,finally
會在控制轉向外部代碼前被執行。
function func() { try { return 1; } catch (err) { /* ... */ } finally { alert( 'finally' ); } } alert( func() ); // 先執行 finally 中的 alert,然後執行這個 alert
try...finally
沒有 catch
子句的 try...finally
結構也很有用。當我們不想在原地處理 error(讓它們掉出去吧),但是需要確保我們啓動的處理需要被完成時,我們應當使用它。
function func() { // 開始執行需要被完成的操作(比如測量) try { // ... } finally { // 完成前面我們需要完成的那件事,即使 try 中的執行失敗了 } }
上面的代碼中,由于沒有 catch
,所以 try
中的 error 總是會使代碼執行跳轉至函數 func()
外。但是,在跳出之前需要執行 finally
中的代碼。
環境特定
這個部分的內容並不是 JavaScript 核心的壹部分。
設想壹下,在 try...catch
結構外有壹個致命的 error,然後腳本死亡了。這個 error 就像編程錯誤或其他可怕的事兒那樣。
有什麽辦法可以用來應對這種情況嗎?我們可能想要記錄這個 error,並向用戶顯示某些內容(通常用戶看不到錯誤信息)等。
規範中沒有相關內容,但是代碼的執行環境壹般會提供這種機制,因爲它確實很有用。例如,Node.JS 有 process.on("uncaughtException")
。在浏覽器中,我們可以將壹個函數賦值給特殊的 window.onerror 屬性,該函數將在發生未捕獲的 error 時執行。
語法如下:
window.onerror = function(message, url, line, col, error) { // ... };
message
error 信息。
url
發生 error 的腳本的 URL。
line
,col
發生 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
的作用通常不是恢複腳本的執行 —— 如果發生編程錯誤,恢複腳本的執行幾乎是不可能的,它的作用是將錯誤信息發送給開發者。
也有針對這種情況提供 error 日志的 Web 服務,例如 https://errorception.com 或 http://www.muscula.com。
它們會像這樣運行:
我們注冊該服務,並拿到壹段 JavaScript 代碼(或腳本的 URL),然後插入到頁面中。
該 JavaScript 腳本設置了自定義的 window.onerror
函數。
當發生 error 時,它會發送壹個此 error 相關的網絡請求到服務提供方。
我們可以登錄到服務方的 Web 界面來查看這些 error。
try...catch
結構允許我們處理執行過程中出現的 error。從字面上看,它允許“嘗試”運行代碼並“捕獲”其中可能發生的 error。
語法如下:
try { // 執行此處代碼 } catch (err) { // 如果發生 error,跳轉至此處 // err 是壹個 error 對象 } finally { // 無論怎樣都會在 try/catch 之後執行 }
這兒可能會沒有 catch
或者沒有 finally
,所以 try...catch
或 try...finally
都是可用的。
Error 對象包含下列屬性:
message
—— 人類可讀的 error 信息。
name
—— 具有 error 名稱的字符串(Error 構造器的名稱)。
stack
(沒有標准,但得到了很好的支持)—— Error 發生時的調用棧。
如果我們不需要 error 對象,我們可以通過使用 catch {
而不是 catch (err) {
來省略它。
我們也可以使用 throw
操作符來生成自定義的 error。從技術上講,throw
的參數可以是任何東西,但通常是繼承自內建的 Error
類的 error 對象。下壹章我們會詳細介紹擴展 error。
再次抛出(rethrowing)是壹種錯誤處理的重要模式:catch
塊通常期望並知道如何處理特定的 error 類型,因此它應該再次抛出它不知道的 error。
即使我們沒有 try...catch
,大多數執行環境也允許我們設置“全局” error 處理程序來捕獲“掉出(fall out)”的 error。在浏覽器中,就是 window.onerror
。
重要程度: 5
比較下面兩個代碼片段。
第壹個代碼片段,使用 finally
在 try..catch
之後執行代碼:
try { // 工作 } catch (err) { // 處理 error } finally { // 清理工作空間 }
第二個代碼片段,將清空工作空間的代碼放在了 try...catch
之後:
try { // 工作 } catch (err) { // 處理 error } // 清理工作空間
我們肯定需要在工作後進行清理,無論工作過程中是否有 error 都不影響。
在這兒使用 finally
更有優勢,還是說兩個代碼片段效果壹樣?如果在這有這樣的優勢,如果需要,請舉例說明。
當我們看函數中的代碼時,差異就變得很明顯了。
如果在這有“跳出” try..catch
的行爲,那麽這兩種方式的表現就不同了。
例如,當 try...catch
中有 return
時。finally
子句會在 try...catch
的 任意 出口處起作用,即使是通過 return
語句退出的也是如此:在 try...catch
剛剛執行完成後,但在調用代碼獲得控制權之前。
function f() { try { alert('start'); return "result"; } catch (err) { /// ... } finally { alert('cleanup!'); } } f(); // cleanup!
……或者當有 throw
時,如下所示:
function f() { try { alert('start'); throw new Error("壹個 error"); } catch (err) { // ... if("無法處理此 error") { throw err; } } finally { alert('cleanup!') } } f(); // cleanup!
正是這裏的 finally
保證了 cleanup。如果我們只是將代碼放在函數 f
的末尾,則在這些情況下它不會運行。