2022 年 10 月 14 日

錯誤處理,「try...catch」

無論我們的程式設計技術有多好,有時我們的腳本還是會有錯誤。它們可能發生在我們犯錯、使用者輸入意外、伺服器回應錯誤,以及其他上千個原因。

通常,腳本會在發生錯誤時「死亡」(立即停止),並將錯誤印出到主控台。

但有一個語法結構 try...catch 允許我們「捕捉」錯誤,因此腳本可以執行更合理的動作,而不是死亡。

「try…catch」語法

try...catch 結構有兩個主要區塊:try,然後是 catch

try {

  // code...

} catch (err) {

  // error handling

}

它的運作方式如下

  1. 首先,執行 try {...} 中的程式碼。
  2. 如果沒有錯誤,則會忽略 catch (err):執行會到達 try 的結尾,然後繼續執行,略過 catch
  3. 如果發生錯誤,則會停止執行 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>

技術上來說,我們可以使用任何東西作為錯誤物件。那甚至可以是一個基本型別,例如數字或字串,但最好使用物件,最好是具有 namemessage 屬性(以與內建錯誤保持一些相容性)。

JavaScript 有許多用於標準錯誤的內建建構函式:ErrorSyntaxErrorReferenceErrorTypeError 等。我們也可以使用它們來建立錯誤物件。

它們的語法為

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" 訊息。這是錯誤的,而且也讓程式碼更難除錯。

為了避免此類問題,我們可以使用「重新拋出」技巧。規則很簡單

捕捉只能處理它知道的錯誤,並「重新拋出」所有其他錯誤。

「重新拋出」技巧可以更詳細地說明為

  1. 捕捉取得所有錯誤。
  2. catch (err) {...} 區塊中,我們分析錯誤物件 err
  3. 如果我們不知道如何處理它,我們會執行 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' );
}

這段程式碼有兩種執行方式

  1. 如果你在「產生錯誤?」回答「是」,則為 try -> catch -> finally
  2. 如果你說「否」,則為 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。兩個測量都正確完成。

換句話說,函式可能會以 returnthrow 結束,這並不重要。finally 子句在這兩種情況下都會執行。

變數在 try...catch...finally 內部是區域性的

請注意,上面程式碼中的 resultdiff 變數是在 try...catch 之前 宣告的。

否則,如果我們在 try 區塊中宣告 let,它只會在區塊內部可見。

finallyreturn

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。
linecol
發生錯誤的行和欄位號碼。
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.comhttps://www.muscula.com

它們的工作方式如下

  1. 我們在服務中註冊,並從中取得一段 JS(或腳本 URL)插入到網頁中。
  2. 該 JS 腳本會設定自訂 window.onerror 函式。
  3. 當發生錯誤時,它會傳送網路要求給服務。
  4. 我們可以登入服務網路介面查看錯誤。

摘要

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...catchtry...finally 也是有效的。

錯誤物件具有下列屬性

  • message – 人類可讀的錯誤訊息。
  • name – 含有錯誤名稱(錯誤建構函式名稱)的字串。
  • stack(非標準,但支援良好) – 錯誤建立時的堆疊。

如果不需要錯誤物件,我們可以使用 catch { 代替 catch (err) { 來省略它。

我們也可以使用 throw 算子產生自己的錯誤。技術上來說,throw 的引數可以是任何東西,但通常是繼承自內建 Error 類別的錯誤物件。有關在下一章節中延伸錯誤的更多資訊。

重新擲回 是錯誤處理中非常重要的模式:catch 區塊通常會預期並知道如何處理特定錯誤類型,因此它應該重新擲回它不認識的錯誤。

即使我們沒有 try...catch,大多數環境允許我們設定一個「全域」錯誤處理常式來捕捉「掉出來」的錯誤。在瀏覽器中,那就是 window.onerror

作業

重要性:5

比較兩個程式碼片段。

  1. 第一個使用 finallytry...catch 之後執行程式碼

    try {
      work work
    } catch (err) {
      handle errors
    } finally {
      cleanup the working space
    }
  2. 第二個片段在 try...catch 之後立即進行清理

    try {
      work work
    } catch (err) {
      handle errors
    }
    
    cleanup the working space

我們絕對需要在工作之後進行清理,無論是否有錯誤。

這裡使用 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("an error");
  } catch (err) {
    // ...
    if("can't handle the error") {
      throw err;
    }

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

f(); // cleanup!

這裡保證清理的是 finally。如果我們只是將程式碼放在 f 的結尾,它不會在這些情況下執行。

教學課程地圖

留言

在留言前請閱讀此內容…
  • 如果您有改進建議,請 提交 GitHub 問題 或提交 pull 要求,而不是留言。
  • 如果您無法理解文章中的某些內容,請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,對於多行,請將它們包覆在 <pre> 標籤中,對於超過 10 行,請使用沙盒 (plnkrjsbincodepen…)