錯誤處理 Error Handling in JS
try…catch 只是第一步
網站發生錯誤的原因百百種,無法完全避免。不過一個產品成功與否,並不是追求極致的 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
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,但不推薦拋出原始型別 (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
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 非同步: 若有程式是非同步像
setTimeout
、promise
那正常的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…catch
跟 throw
的範例如下
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.
- 在第三方 library 的載入前(例如 jQuery) 就使用它的變數
- 更多完整範例
想避免 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
- 想要取型別為
undefined
或null
裡面的屬性
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 (例如 lerrorception 或 muscula )藉由 log errors 可以追蹤網站的錯誤,列出發生錯誤的原因,以及提供詳細錯誤資訊、分析錯誤的 pattern 等等能加速開發團隊除錯,例如當一個特定的 API 連結時不時就連不到,就代表它真的有問題!這比起 QA 團隊偵測 (或最糟的狀況是使用者發現) 更有效率。
補充: 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