跳至主要内容

[JS] Error Handling

keywords: exception handling, javascript, 錯誤處理, 例外處理

Error Handling 的重要概念

參考資料
  • 說明可能 catch 不到 error 的情況:try...catch @ javascript.info

Uncaught Error

當錯誤都沒有被處理時,最終會拋到最外層,這時候:

  • Node.js: process.on('uncaughtException') 會被觸發
  • 瀏覽器:window.onerror = function(message, url, line, col, error){} 會被觸發

Custom Error

create custom error class
// error.ts

export class ApiError extends Error {
status: number;

constructor(url: string, status: number) {
super(`Request fail with ${status} on ${url}`);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, ApiError);
}

this.name = 'ApiError';
this.status = status;
}
}
throw custom error
// fetch.ts

export async function fetchJson<T>(url: string): Promise<T> {
const resp = await fetch(url);

if (!resp.ok) {
throw new ApiError(url, resp.status);
}

const data = await resp.json();
return data;
}
use custom error
const fetchProducts = async () => {
try {
const products = await fetchJson('http://example.com');
// ...
} catch (error) {
if (error instanceof ApiError) {
// handle Api Error
// we can use `error.name` and `error.status` in the Custom Error now
}
throw error;
}
};

JavaScript Error Handling Pattern

Either Pattern

參考資料
// return the function with tuple [Data?, Error?]
async function foo() {
try {
const data = await promise;
return [data, null];
} catch (error) {
return [null, error];
}
}

async function main() {
const [data, error] = await foo();

if (error) {
/* ... */
}

const [data, error] = await bar();
}

範例程式碼

// fetch.ts
import { ApiError } from './error';

type Success<T> = [T, null];
type Fail = [null, Error];

export async function fetchJson<T>(
url: string,
options?: RequestInit | undefined,
): Promise<Success<T> | Fail> {
try {
const resp = await fetch(url, options);

if (!resp.ok) {
throw new ApiError(url.toString(), resp.status);
}

const data = await resp.json();
return [data, null];
} catch (error) {
if (error instanceof Error) {
return [null, error];
}

throw error;
}
}
export async function getProducts() {
const [products, error] = await fetchJson<ApiProduct[]>(`${CMS_URL}/products`);

if (error) {
throw error;
}

return products.map(mapProduct);
}

JavaScript Error Object

  • try...catch... 的使用應該是用來處理「例外情況」,也就是沒有預期到會發生的事,不要把它當作 conditional flow 使用。
// Error Object
function throwError() {
try {
throw new Error('foobar');
} catch (ex) {
console.log(ex); // Error: foobar
console.log(ex.name); // Error;
console.log(ex.message); // foobar;
}
}

throwError();

本文主要內容翻譯自 Exceptional Exception Handling in JavaScript @ SitePoint

在撰寫程式的過程中發生錯誤(error)或出現例外情況(exception)是經常出現的情況,一般我們會把「錯誤」稱作「例外」,兩者可以交替著使用。

當 JavaScript 程式執行的過程中發生錯誤時,它會丟出例外狀況(throw an exception),JS 並不會繼續往下走,而是尋找有沒有任何程式碼能夠處理這些錯誤(exception handling code),如果沒有找到任何可以處理錯誤狀況的程式碼,則它會從丟出例外狀況的函式中跳出(return),就這樣重複尋找錯誤處理的程式碼、跳出,直到觸及到最外層的函式(top level function)後終止。

錯誤類型

當例外產生時,會產生一個用來代表錯誤的物件。在 JS 中內建了 7 種錯誤類型的物件:

1. Error

Error @ MDN > Standard build-in objects

Error 這個類型用來代表一般的錯誤情況,最常用在客製化例外情況。透過下面的指令可以產生一個錯誤物件的實例:

var error = new Error('error message');
console.log(error); // Error: error message

這個 Error 物件包含兩個屬性-namemessage

  • name: 用來說明錯誤的類型(這裡就是 Error
  • message: 提供更多關於此例外情況的描述。

2. RangeError

RangeError 的例外情況會發生在當數值落在特定的區間外時,例如,透過 toFixed() 方法時,它可以接受介於 0 ~ 20 的參數來說明要顯示到小數後第幾位,當這個參數超過這個區間時,就會拋出 RangeError

var pi = 3.14159;
pi.toFixed(100000); // RangeError: toFixed() digits argument must be between 0 and 100

3. ReferenceError(找不到變數:拼錯字)

當試圖存取一個不存在的變數時,會拋出 ReferenceError,這個錯誤經常發生在拼錯字的情況。

console.log(bar); // ReferenceError: bar is not defined

4. SyntaxError(語法錯誤)

當有程式碼違反 JavaScript 的語法規則時,會拋出 SyntaxError 的錯誤。熟悉 C 或 Java 的通常是在編譯的過程中(computation process)遇到語法錯誤;但 JavaScript 是直譯式語言(interpreted language),因此當程式碼被執行到時,才會辨認到語法錯誤。這種錯誤類型是所有例外狀況中唯一不能被修復的

if (false) {    // SyntaxError: Unexpected token (3:0)
// 缺少結束的大括號

5. TypeError(找不到函式)

如果某一變項的型別和所期待的操作不同時,會拋出 TypeError這經常發生在去呼叫執行一個不存在的函式時

/**
* 由於 foo 當中並不包含 bar 這個函式,因此會拋出 TypeError 錯誤
**/
var foo = {};
foo.bar(); // TypeError: foo.bar is not a function

6. URIError

當使用 encodeURI()decodeURI() 的方法,但確有給了不合法的 URI 時會拋出這個錯誤:

/**
* "%" 表示的是 URI 中的跳脫片段,但在下面的例子中 "%" 後沒有接任和字串,因此是不合法的跳脫片段
**/
decodeURIComponent('%'); // URIError: URI malformed

7. EvalError

eval() 這個函式不恰當的使用時,會拋出 EvalError 這個錯誤。這個錯誤不再被當前的 ECMAScript 規範所採用。

處理錯誤(Handling Exceptions)

在 JavaScript 中如果碰到錯誤或例外情況時,如果沒有找到錯誤處理的程式,它會直接在那裡炸掉。那麼要如何避免錯誤產生時讓我們的程式直接炸掉呢?在 JavaScript 中,可以使用 try...catch...finally 語句。

try {
// attempt to execute this code
} catch (exception) {
// this code handles exceptions
} finally {
// this code always gets executed
}

try

我們預期在 try 區塊內的程式碼會成功執行,但當有 try 區塊中有任何錯誤發生時,會立即進入 catch 的區塊;如果沒有錯誤發生,則會跳過 catch 區塊。finally 則是會在 try...catch... 之後先被執行。

catch

catch 區塊中可以指定一個參數,這個參數通常稱作 exceptioncatchID。在 catch 區塊中可以辨認這個參數,但離開這個區塊後就無法取得這個變數。透過 catch 可以阻止例外情況繼續向外冒泡,讓整個程式可以繼續執行。

try {
foo++; // ReferenceError
} catch (exception) {
// ReferenceError: foo is not defined
console.log(`${exception.name}: ${exception.message}`);
}

finally

finally 區塊中的程式碼會在 try, catch 後被執行,不論有沒有例外情況產生,因此 finally 區塊通常用來包含清除的程式碼(例如,closing files)。

但是如果 finally 區塊中有 return 值的話,這個值會是最後整個 try-catch-finally 回傳的結果,即時在 trycatch 中有 return 或其他的 throw 都會被忽略。

function f() {
try {
console.log(0);
throw 'bogus'; // 進入 catch
} catch (e) {
console.log(1);
return true; // 這個回傳值會被終止,直到整個 finally block 完成之後
console.log(2); // 執行不到
} finally {
console.log(3);
return false; // finally 中的 return 會覆蓋掉 try/catch 中的 return 或 throw
console.log(4); // 執行不到
}
// 執行 finally 中的 "return false"
console.log(5); // 執行不到
}
f(); // 0, 1, 3; returns false

客製化錯誤(Throwing Exceptions)

JavaScript 允許開發者透過 throw 來拋出客製化的例外情形:

throw expression;

throw 'Error2'; // String type
throw 42; // Number type
throw true; // Boolean type

透過 throw 可以丟出幾乎任何型別的錯誤,但一般還是建議使用內建的例外類型(瀏覽器會提供比較多資訊)。

客製化錯誤訊息(message)

例如,當我們透過除法卻不小心除了分母為 0 的數值時,可以使用 throw 來拋出例外情況:

let denominator = 0;

// RangeError: Attempted division by zero!
try {
if (denominator === 0) {
throw new RangeError('Attempted division by zero!');
}
} catch (e) {
console.log(e.name + ': ' + e.message);
}

客製化錯誤類別

透過上面的方式可以根據使用原生的錯誤類型(e.name)給予想要的錯誤訊息(e.message),但如果我們想要客製化錯誤類別的話則可以這麼做:

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling#throw_statement

/**
* 客製化錯誤類型
**/
function DivisionByZeroError(message) {
this.name = 'DivisionByZeroError';
this.message = message;
}

// 使用 console.log 或 console.error 時能夠輸出字串
DivisionByZeroError.prototype.showError = function () {
return this.name + ': "' + this.message + '"';
};

使用客製化錯誤類型:

try {
throw new DivisionByZeroError('Attempted division by zero!');
} catch (err) {
console.log(err.showError()); // DivisionByZeroError: "Attempted division by zero!"
console.log(err instanceof Error); // false
console.llg(err instanceof DivisionByZeroError); // true
}

程式範例 @ JSFiddle Why need to assign constructor back to my custom error type in JavaScript @ StackOverflow

其他範例

透過 instanceof 處理多種不同的錯誤類型

利用 instanceof 可以根據不同的錯誤類型執行不同的 catch

try {
// assume an exception occurs
} catch (exception) {
if (exception instanceof TypeError) {
// Handle TypeError exceptions
} else if (exception instanceof ReferenceError) {
// Handle ReferenceError exceptions
} else {
// Handle all other types of exceptions
}
}

參考

待閱讀