給急躁的程式設計師的 JavaScript(ES2022 版)
請支持這本書:購買捐款
(廣告,請不要封鎖。)

24 例外處理



本章節涵蓋 JavaScript 如何處理例外。

  為什麼 JavaScript 沒有更常拋出例外?

JavaScript 直到 ES3 才支援例外。這說明了為什麼語言及其標準函式庫很少使用它們。

24.1 動機:拋出和捕捉例外

考慮以下程式碼。它會將儲存在檔案中的個人資料讀取到陣列中,其中包含 Profile 類別的執行個體

function readProfiles(filePaths) {
  const profiles = [];
  for (const filePath of filePaths) {
    try {
      const profile = readOneProfile(filePath);
      profiles.push(profile);
    } catch (err) { // (A)
      console.log('Error in: '+filePath, err);
    }
  }
}
function readOneProfile(filePath) {
  const profile = new Profile();
  const file = openFile(filePath);
  // ··· (Read the data in `file` into `profile`)
  return profile;
}
function openFile(filePath) {
  if (!fs.existsSync(filePath)) {
    throw new Error('Could not find file '+filePath); // (B)
  }
  // ··· (Open the file whose path is `filePath`)
}

讓我們檢查 B 行中發生了什麼事:發生錯誤,但處理問題的最佳位置不是目前的位置,而是 A 行。在那裡,我們可以跳過目前的檔案並繼續處理下一個檔案。

因此

當我們拋出時,以下建構會處於作用中

readProfiles(···)
  for (const filePath of filePaths)
    try
      readOneProfile(···)
        openFile(···)
          if (!fs.existsSync(filePath))
            throw

throw 會逐一退出巢狀建構,直到它遇到 try 陳述式。執行會繼續在該 try 陳述式的 catch 子句中。

24.2 throw

以下是 throw 語句的語法

throw «value»;

24.2.1 我們應該擲出哪些值?

在 JavaScript 中,可以擲出任何值。然而,最好使用 Error 或其子類別的實例,因為它們支援其他功能,例如堆疊追蹤和錯誤鏈結(請參閱 §24.4 “Error 及其子類別”)。

這讓我們有以下選項

24.3 try 語句

try 語句的最大版本如下所示

try {
  «try_statements»
} catch (error) {
  «catch_statements»
} finally {
  «finally_statements»
}

我們可以將這些子句組合如下

24.3.1 try 區塊

try 區塊可以被視為語句的主體。這是我們執行一般程式碼的地方。

24.3.2 catch 子句

如果例外狀況到達 try 區塊,則會將其指定給 catch 子句的參數,並執行該子句中的程式碼。接下來,執行通常會在 try 語句之後繼續。如果發生下列情況,這可能會改變

以下程式碼示範 A 行中擲出的值確實會在 B 行中被捕獲。

const errorObject = new Error();
function func() {
  throw errorObject; // (A)
}

try {
  func();
} catch (err) { // (B)
  assert.equal(err, errorObject);
}
24.3.2.1 省略 catch 繫結 [ES2019]

如果我們對擲出的值不感興趣,則可以省略 catch 參數

try {
  // ···
} catch {
  // ···
}

這偶爾可能會很有用。例如,Node.js 有 API 函式 assert.throws(func),它會檢查 func 內部是否擲出錯誤。它可以實作如下。

function throws(func) {
  try {
    func();
  } catch {
    return; // everything OK
  }
  throw new Error('Function didn’t throw an exception!');
}

然而,此函式的更完整實作會有 catch 參數,並會檢查其類型是否如預期般,例如。

24.3.3 finally 子句

finally 子句內的程式碼總是在 try 語句的結尾執行,無論在 try 區塊或 catch 子句中發生什麼事。

讓我們看看 finally 的常見使用案例:我們已建立一個資源,並希望在我們使用完畢後始終銷毀它,無論在使用過程中發生什麼事。我們會實作如下

const resource = createResource();
try {
  // Work with `resource`. Errors may be thrown.
} finally {
  resource.destroy();
}
24.3.3.1 finally 總是會執行

即使發生錯誤(A 行),finally 子句也會執行

let finallyWasExecuted = false;
assert.throws(
  () => {
    try {
      throw new Error(); // (A)
    } finally {
      finallyWasExecuted = true;
    }
  },
  Error
);
assert.equal(finallyWasExecuted, true);

即使有 return 陳述式(A 行)

let finallyWasExecuted = false;
function func() {
  try {
    return; // (A)
  } finally {
    finallyWasExecuted = true;
  }
}
func();
assert.equal(finallyWasExecuted, true);

24.4 Error 及其子類別

Error 是所有內建錯誤類別的共同父類別。

24.4.1 類別 Error

以下是 Error 的實例屬性和建構函式的樣子

class Error {
  // Instance properties
  message: string;
  cause?: any; // ES2022
  stack: string; // non-standard but widely supported

  constructor(
    message: string = '',
    options?: ErrorOptions // ES2022
  );
}
interface ErrorOptions {
  cause?: any; // ES2022
}

建構函式有兩個參數

下一個子節之後的子節會更詳細地說明實例屬性 .message.cause.stack

24.4.1.1 Error.prototype.name

每個內建錯誤類別 E 都有一個屬性 E.prototype.name

> Error.prototype.name
'Error'
> RangeError.prototype.name
'RangeError'

因此,有兩種方法可以取得內建錯誤物件的類別名稱

> new RangeError().name
'RangeError'
> new RangeError().constructor.name
'RangeError'
24.4.1.2 Error 實例屬性 .message

.message 僅包含錯誤訊息

const err = new Error('Hello!');
assert.equal(String(err), 'Error: Hello!');
assert.equal(err.message, 'Hello!');

如果我們省略訊息,則會使用空字串作為預設值(繼承自 Error.prototype.message

如果我們省略訊息,它會是空字串

assert.equal(new Error().message, '');
24.4.1.3 Error 實例屬性 .stack

實例屬性 .stack 不是 ECMAScript 功能,但它受到 JavaScript 引擎的廣泛支援。它通常是字串,但其確切結構未標準化,且因引擎而異。

以下是它在 JavaScript 引擎 V8 上的樣子

const err = new Error('Hello!');
assert.equal(
err.stack,
`
Error: Hello!
    at file://ch_exception-handling.mjs:1:13
`.trim());
24.4.1.4 Error 實例屬性 .cause [ES2022]

實例屬性 .cause 是透過 new Error() 第二個參數中的選項物件建立的。它指定導致目前錯誤的其他錯誤。

const err = new Error('msg', {cause: 'the cause'});
assert.equal(err.cause, 'the cause');

有關如何使用此屬性的資訊,請參閱 §24.5「串連錯誤」

24.4.2 Error 的內建子類別

Error 有以下子類別 - 引用 ECMAScript 規範

24.4.3 子類化 Error

自 ECMAScript 2022 以來,Error 建構函式接受兩個參數(請參閱前一個子節)。因此,當我們對其進行子類化時,我們有兩種選擇:我們可以在我們的子類別中省略建構函式,或者我們可以像這樣呼叫 super()

class MyCustomError extends Error {
  constructor(message, options) {
    super(message, options);
    // ···
  }
}

24.5 鏈結錯誤

24.5.1 我們為何要鏈結錯誤?

有時,我們會捕捉在更深層巢狀函式呼叫期間引發的錯誤,並希望附加更多資訊給它

function readFiles(filePaths) {
  return filePaths.map(
    (filePath) => {
      try {
        const text = readText(filePath);
        const json = JSON.parse(text);
        return processJson(json);
      } catch (error) {
        // (A)
      }
    });
}

try 子句內的陳述式可能會引發各種錯誤。在多數情況下,錯誤不會知道導致它的檔案路徑。這就是我們希望在 A 行附加該資訊的原因。

24.5.2 透過 error.cause 鏈結錯誤 [ES2022]

自 ECMAScript 2022 以來,new Error() 讓我們可以指定導致它的原因

function readFiles(filePaths) {
  return filePaths.map(
    (filePath) => {
      try {
        // ···
      } catch (error) {
        throw new Error(
          `While processing ${filePath}`,
          {cause: error}
        );
      }
    });
}

24.5.3 .cause 的替代方案:自訂錯誤類別

下列自訂錯誤類別支援鏈結。它與 .cause 向前相容。

/**
 * An error class that supports error chaining.
 * If there is built-in support for .cause, it uses it.
 * Otherwise, it creates this property itself.
 *
 * @see https://github.com/tc39/proposal-error-cause
 */
class CausedError extends Error {
  constructor(message, options) {
    super(message, options);
    if (
      (isObject(options) && 'cause' in options)
      && !('cause' in this)
    ) {
      // .cause was specified but the superconstructor
      // did not create an instance property.
      const cause = options.cause;
      this.cause = cause;
      if ('stack' in cause) {
        this.stack = this.stack + '\nCAUSE: ' + cause.stack;
      }
    }
  }
}

function isObject(value) {
  return value !== null && typeof value === 'object';
}

  練習:例外處理

exercises/exception-handling/call_function_test.mjs

  測驗

請參閱 測驗應用程式