錯誤處理 Error Handling in JS

try…catch 只是第一步

Hannah Lin
Hannah Lin
20 min readMar 25, 2024

--

網站發生錯誤的原因百百種,無法完全避免。不過一個產品成功與否,並不是追求極致的 0 錯誤 (也不可能),而是取決於是否妥當處理錯誤,讓使用者體驗不至於太糟糕 (不然使用者可是會不留情面轉頭就走)。

錯誤處理的層面很廣,這篇只會著重於前端的 JS 部分,從認識 JS 錯誤、如何處理錯誤、不同錯誤類別、到 Best Practice for handling Errors,期許自己在下一個案子能更專業的去處理這些錯誤。

🔖 文章索引

1. What Are JavaScript Errors?
2. How to Identify and Prevent Errors in JavaScript
3. Types of Errors in JavaScript
4. Handle Errors Globally with `onerror`
5. Best Practice for handling Errors in FrontEnd

What Are JavaScript Errors?

當 program 不知道如何處理任務時就會 throw errors。這個 error 物件包含 name (錯誤類別)、message(描述錯誤)、stack (錯誤發生在哪行程式碼) 等有用資訊提供開發者除錯

JS 是 single thread ,所以當遇到錯誤 (program 不知道如何處理任務) 時就會無法往下執行,整個網站掛掉。造成異常原因百百種,例如執行一個不存在的 function、reach out 一個連不到的 API endpoint (e.g. server 掛掉或連太久)、輸入預期外的 input 資料 (包含使用者在你的 form 上面亂打) 等狀況就會導致錯誤而無法繼續執行剩下的程式。

典型的錯誤例子

當錯誤發生,JS 會產生一個 Errors 物件,此物件收集跟這錯誤有關的屬性: name、message、stack 提供開發者除錯

  • name : 這個錯誤的類別名稱,例如 ReferenceError(可以看這裡 Types of Errors in JavaScript)
  • message: 描述造成此錯誤的原因,每個瀏覽器的 message 也會不同
ReferenceError: "hi" is not defined (V8-based & Firefox)
ReferenceError: Can't find variable: hi (Safari)
  • stack: 列出巢狀詳細資訊告訴你錯誤在哪裡,通常用來 debug
stack 範例
try {
throw new SyntaxError("Hello");
} catch (err) { // <-- the "error object"
console.log(err instanceof SyntaxError); // true
console.log(err.message); // "Hello"
console.log(err.name); // "SyntaxError"
console.log(err.stack); // SyntaxError: Hello at (...call stack)
}

How to Identify and Prevent Errors in JavaScript

Throwing your own errors (exceptions)

除了 JS 內建遇到無法處理任務時會自動拋出異常外 ; JS 也提供try…catch 語法讓開發者能控制丟出錯誤的時機,以及相對應處理錯誤的方法,讓網站就算遇到錯誤,也不至於直接掛掉,使用者甚至還可以繼續操作網站其他功能。

該怎麼做呢 ? 其實很簡單,就是在有可能發生錯誤的地方包上 try…catch,並在 try 區塊裡丟出自定義錯誤 throw new Error(“錯誤訊息”)

try {
// code...
if(xxx){
throw new Error("錯誤訊息"); // 自訂義錯誤
}
} catch ( error ) {
// error handling
}

throw

the throw keyword generates an error manually

你可以 throw 任何 data type

雖然 throw 可以丟出任何 data type,但不推薦拋出原始型別 (number, string…),因為此異常不會有 name, message 跟 stack 這種幫助你除錯的資訊。所以較好做法還是使用 Error Object (note. 以下範例的 Error 可以改成任何 Error type)

throw new Error(“error message here”)

function check(string){
if(!string){
throw new Error("内容不存在");
// or
throw new TypeError("message")
// ...
}
}

但錯誤若沒被包進 try…catch 會導致整個網站掛掉,所以還需要知道另一個關鍵字 try…catch

without try catch

try…catch

try…catch 語法讓我們在整個網站掛掉前先 catch 錯誤

try {
// run this code
} catch (err) {
// if an error happened, then jump here
} finally {
// execute always
}
  • try: 有可能發生錯誤的程式碼區塊
  • catch: 處理異常的區塊。err 是 error object,若你不需要錯誤相關資訊也可以用 catch {} 取代 catch (err) {}
  • finally(optional): 一定會執行的區塊。若沒有錯誤發生: try -> finally; 發生錯誤:try -> catch -> finally

程式執行過程如圖,包在 try 裡的程式碼會先執行,如果

  • 沒有錯誤: 執行完 try 裡程式碼後不會執行 catch 區塊
  • 發生錯誤: 不會執行剩下在 try 裡的程式,而是會跳到 catch 區塊處理異常,catch(err) 中的 err 會是攜帶詳細錯誤資訊 error object
// An errorless example
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)
}


// Have error example
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 {…} 中的錯誤造成網站死當,而是妥善處理它,讓 program 可以繼續執行。但要特別注意以下兩種常見情況 try…catch 是無法 處理的:

  • syntactically wrong 語法錯誤: 程式必須是 valid JavaScript,若有語法錯誤,連 try 區塊都無法執行那就不用說了
try {
{{{{{{{{{{{{ // 程式直接 die here, the engine can’t understand the code.
} catch (err) {
alert("The engine can't understand this code, it's invalid");
}
  • asynchronously code 非同步: 若有程式是非同步像 setTimeoutpromise 那正常的 try…catch 是抓不到當中發生的錯誤的,因為當輪到執行非同步程式碼時,the engine 早就離開了 try…catch 區塊 (延伸閱讀: Event loop)。
try {
setTimeout(function() {
noSuchVariable; // script will die here
}, 1000);
} catch (err) {
alert( "won't work" );
}

但這不代表非同步程式碼的錯誤就無法處理,而是要寫對位置 (延伸閱讀: Error handling with promises)

// work!
setTimeout(function() {
try {
noSuchVariable; // try...catch handles the error!
} catch {
alert( "error is caught here!" );
}
}, 1000);


async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}

一個完整包含 try…catchthrow 的範例如下

function getRectArea(width, height) {
try{
if (isNaN(width) || isNaN(height)) {
throw new Error("Parameter is not a number!");
};
} catch(e){
console.error(`custome msg here ${e}`)
}
};
console.log(getRectArea('abc', 123));
console.log('end');
// custome msg here Error: Parameter is not a number!
// end

Types of Errors in JavaScript

除了使用 throw new Error(“error message here”) 拋出異常外,Error 可以被替代為任何 Error type

throw new Error("xxx")
throw new RangeError("xxx")
throw new InternalError("xxx")
...

Error Object 的 name 也是 Error type。

內建 error 的Error type 很多,這邊就只說前端最常見的部分

SyntaxError (missing semicolon in your code)

when there is a typo in the code, creating invalid code.
語法錯誤導致程式碼無法被解析

const func = () => {
console.log(hello)
// SyntaxError: Unexpected token }

var 2b;
// SyntaxError: Invalid or unexpected token

const = "foo";
// SyntaxError: Unexpected token '='

常見的 SyntaxError :

  • 少了開始或結束的 brackets [], braces {}, parentheses ()
  • 少了或 invalid semicolons;
  • 變數或函示名稱 invalid
  • 更多完整範例

SyntaxError 算是相對容易解決的錯誤,通常只要補齊符號或是修正寫錯的變數就可以解決。在 editor 使用 linting tool 也可以避免這種錯誤發生。

ReferenceError (using a variable that has not been defined)

a variable is used that does not exist.
使用不存在的變數

console.log(x)
// ReferenceError: x is not defined

hi()
// ReferenceError: hi is not defined

常見的 ReferenceError :

  • 使用未定義的函示或變數
  • 在 lexical scope 外使用 scope 內的變數 (就會抓不到)
function numbers() {
const num1 = 2;
const num2 = 3;
return num1 + num2;
}

console.log(num1); // ReferenceError num1 is not defined.
  • 把值指二次派給 const 變數 (const 是常數無法被更改)
const a = 5;
a = 4;
// TypeError: Assignment to constant variable.

想避免 ReferenceError 錯誤,就要確保所有的變數在使用前先定義好值(或已初始化)。

TypeError

value doesn’t turn out to be of a particular expected type.
變數在操作時不是預期型別導致錯誤發生

const func = () => { console.o()};
func();
// TypeError: console.o is not a function

let num = 15;
console.log(num.split(""));
// TypeError: num.split is not a function

常見的 TypeError :

  • 使用根本不存在此型別的方法 (例如 array 使用 object method; object 使用 array method)
const obj = { a: 13, b: 37, c: 42 };

obj.map(function (num) {
return num * 2;
});

// TypeError: obj.map is not a function

[1, 3, 2].sort(5);
// TypeError: The comparison function must be either a function or undefined
  • 想要取型別為 undefinednull 裡面的屬性
null.foo;
// TypeError: null has no properties

undefined.bar;
// TypeError: undefined has no properties

var list;
list.count;
// TypeError: Cannot read properties of undefined

RangeError

when value is not in the set or range of allowed values.
超出有效範圍的值被賦與產生的錯誤

常發生在傳錯誤的值給函示的參數時

var a= new Array(-1);
// RangeError: Invalid array length

new Date("2014-25-23").toISOString();
// RangeError: Invalid time value

常見的 RangeError :

  • 傳錯誤的參數給函示的參數時
  • 把錯誤參數傳進 Array
new Array(Math.pow(2, 40));
new Array(-1);
// RangeError: Invalid array length
  • 錯誤參數傳進數字 methods
(77.1234).toExponential(-1); // RangeError
(77.1234).toExponential(101); // RangeError

(2.34).toFixed(-100); // RangeError
(2.34).toFixed(1001); // RangeError

(1234.5).toPrecision(-1); // RangeError

InternalError

this error occurs most often when there is too much data and the stack exceeds its critical size
當太多 recursion 或需要執行的 stack 多到程式無法負荷造成的錯誤

switch(condition) {
case 1:
...
break
case 2:
...
break
case 3:
...
break
case 4:
...
break
case 5:
...
break
case 6:
...
break
case 7:
...
break
... up to 500 cases
}
// InternalError

這個錯誤通常在開發階段就會發現 (頁面完全死當了就是這個錯誤),通常是程式進入了無限迴圈出不來,所以開發者必須避免這種狀況發生。

Creating Custom Error Types

大部分狀況,使用內建 Error Types 就可以符合需求,但開發者還是可以建立客製 Error Types (延伸閱讀: Custom errors, extending Error)

Handle Errors Globally with `onerror`

Errors that occur during the loading of the script can be tracked in an error event.

不管我們寫得再好,一定還是有漏網之魚讓開發者難以捕捉錯誤,所以網站還是需要有 global 抓錯誤的機制,並 etc. )

JS 有 onerror,只要 script 在 loading 時發生錯誤就會觸發這個 onerror 的event,這個 event 包含:

  • Error message: 錯誤訊息
  • URL: 發生錯誤的檔案
  • line, col: 錯誤發生的行數
  • error: error object 也就是之前篇幅提過的 JS Errors 物件
window.onerror = function (message, url, line, col, error) {
if (errorCanBeHandled) {
console.log(`${message}\n At ${line}:${col} of ${url}`)
// display an error message to the user
displayErrorMessage(message);
// return true to short-circuit default error behavior
return true;
} else {
// run the default error handling of the browser
return false;
}
}

onerror不會修復你的 error,而是秀出錯誤資訊讓開發者可以依據這些資訊,客製任何妥善處理錯誤的方法,例如傳 log 訊息給工程師除錯並在 UI 上導回某頁防止整個網站死當。

Note. 不同 JS 框架也會有不同 Handle Errors Globally 的做法,像 React 就有 ErrorBoundary (延伸閱讀: How to handle errors in React: full guide) 幫忙抓錯誤並選擇對應的處理方式。

Best Practice for handling Errors in FrontEnd

除了try…catch 這種實務上的寫法,還有一些 tips 必須了解才能更全面處理錯誤。

善用工具

在開發階段使用 TS 、eslint 等工具能就避免像拼錯或少了 }符號,導致 SyntaxError 這種低階錯誤。

避免 silent failures

有些狀況是程式沒出錯就繼續做事,若抓到異常 (catch) 就什麼都不做

try {
// run this code
} catch() {
// do nothing
}

但這會導致若有新的邏輯在 try 裡卻有異常時難以 debug (silent failures),所以要特別小心處理這種狀況。

提供適合的錯誤訊息給使用者

當錯誤發生時,提供一個清楚並有幫助的訊息給使用者很重要。千萬不要像以下直接把程式自動產生的錯誤訊息顯示給使用者

訊息內容也不是越詳細越好 (使用者對於實際上為何發生此錯誤根本關心),而是提供適合的訊息,怎樣是適合訊息? 他包含

  • 對使用者有幫助: 如果你已經知道為何發生此錯誤,應該提供解決方法給使用者,例如請使用者重新整理 、登出再登入等等。

如果這個錯誤暫時不影響使用者,甚至可以不告知使用者(使用者知道也沒幫助),先把錯誤記錄起來(logging the error) 後再解決。

  • 不能出現敏感資訊 : 避免敏感資訊出現在錯誤訊息裡,例如密碼、API keys、或其他私密資訊。下圖就是一個很好例子

集中管理 logs 跟 Error Alerts

有許多好用的 web-services (例如 lerrorceptionmuscula )藉由 log errors 可以追蹤網站的錯誤,列出發生錯誤的原因,以及提供詳細錯誤資訊、分析錯誤的 pattern 等等能加速開發團隊除錯,例如當一個特定的 API 連結時不時就連不到,就代表它真的有問題!這比起 QA 團隊偵測 (或最糟的狀況是使用者發現) 更有效率。

rollbar.com

補充: Errors vs Exceptions

An exception is an error object that has been thrown

在別的語言中這兩者差異蠻大的,但在 JS 裡我是覺得這觀念不太重要 ?!

// 這是一個錯誤 Error
const wrongTypeError = TypeError("Wrong type found, expected character")

// Exceptions = 拋出錯誤
throw wrongTypeError

--

--