2017年12月7日

[JS] 談談 JavaScript 中的錯誤處理 Error Handling

圖片來源:Proper Error Handling in JavaScript @ Scotch
keywords: exception handling, javascript, 錯誤處理, 例外處理
本文主要內容翻譯自 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 這個類型用來代表一般的錯誤情況,最常用在客製化例外情況。透過下面的指令可以產生一個錯誤物件的實例:
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 的通常是在編譯的過程中(compulation 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 可以丟出幾乎任何型別的錯誤,但一般還是建議使用內建的例外類型(瀏覽器會提供比較多資訊)。

客製化錯誤訊息(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)
}

客製化錯誤名稱(name)

透過上面的方式我們可以根據原生的那 7 種錯誤類型(e.name)給予想要的錯誤訊息(e.message),但如果我們想要客製化錯誤名稱的話可以這麼做:
/**
 * 客製化錯誤類型
 **/
function DivisionByZeroError(message) {
  this.name = 'DivisionByZeroError';
  this.message = message;
}

// 繼承 Error 物件
DivisionByZeroError.prototype = new Error();
DivisionByZeroError.prototype.constructor = DivisionByZeroError;

// 建立 showError 的方法
DivisionByZeroError.prototype.showError = function() {
  return this.name + ': "' + this.message + '"';
}
使用客製化錯誤類型:
let denominator = 0

try {
  if (denominator === 0) {
    throw new DivisionByZeroError("Attempted division by zero!");
  }
}
catch (e) {
  console.log(e.showError())  // DivisionByZeroError: "Attempted division by zero!"
}

其他範例

透過 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
  }
}

參考

0 意見:

張貼留言