博客 / 詳情

返回

漲見識了,Error.cause 讓 JavaScript 錯誤調試更輕鬆

1. 前言

在 JavaScript 中,拋出錯誤很容易,但追溯原因卻有些麻煩,這就是 cause屬性的用武之地。

這是我們傳統的處理錯誤的做法:

try {
  JSON.parse("{ bad json }");
} catch (err) {
  throw new Error("Something went wrong: " + err.message);
}

雖然包裝了錯誤,但已經丟失了原始的堆棧信息和錯誤類型。

當問題發生時,你只能看到最頂層的錯誤信息,卻不知道根本原因是什麼。

你好,我是冴羽。前端資訊、前端乾貨,歡迎關注公眾號:冴羽

2. 引入 Error.cause

ES2022 引入了 Error.cause 屬性,可以保留原始錯誤信息:

try {
  try {
    JSON.parse("{ bad json }");
  } catch (err) {
    throw new Error("Something went wrong", { cause: err });
  }
} catch (err) {
  console.error(err.stack);
  console.error("Caused by:", err.cause.stack);
}

此時你可以看到完整的錯誤鏈:

Error: Something went wrong
    at ...
Caused by: SyntaxError: Unexpected token b in JSON at position 2
    at JSON.parse (<anonymous>)
    at ...

現在,你既保留了原始錯誤,又能提供清晰的頂層錯誤信息。

3. 實際應用示例

讓我們看一個更實際的例子:

function fetchUserData() {
  try {
    JSON.parse("{ broken: true }"); // ← 這裏會失敗
  } catch (parseError) {
    throw new Error("Failed to fetch user data", { cause: parseError });
  }
}

try {
  fetchUserData();
} catch (err) {
  console.error(err.message); // "Failed to fetch user data"
  console.error(err.cause); // [SyntaxError: Unexpected token b in JSON]
  console.error(err.cause instanceof SyntaxError); // true
}

可以看到代碼非常清晰直觀。

而且 cause 屬性被定義為不可枚舉,因此它不會污染日誌或 for...in 循環,除非你顯式訪問它。

4. 自定義錯誤類

你可以在自定義錯誤類中使用 cause 屬性:

class DatabaseError extends Error {
  constructor(message, { cause } = {}) {
    super(message, { cause });
    this.name = "DatabaseError";
  }
}

如果你的運行環境是 ES2022+,這已經足夠了:super(message, { cause }) 會自動處理一切。

對於 TypeScript 用户,確保 tsconfig.json 配置了:

{
  "compilerOptions": {
    "target": "es2022",
    "lib": ["es2022"]
  }
}

否則,在將 { cause } 傳遞給 Error 構造函數時可能會看到類型錯誤。

5. 更好的測試斷言

假設你的服務拋出了一個 UserCreationError,這是由一個 ValidationError 引發的。

你可以這樣寫斷言:

expect(err.cause).toBeInstanceOf(ValidationError);

這樣測試會更清晰、更健壯。

6. 注意事項

默認情況下,console.error(err) 只會打印頂層錯誤。cause鏈不會自動顯示,因此需要手動打印:

console.error(err);
console.error("Caused by:", err.cause);

儘管 cause 很好,但也不要濫用。每個小錯誤都包裝可能更亂,因此只在真正需要上下文的時候使用。

7. 遞歸打印完整錯誤鏈

這是一個安全遍歷錯誤鏈的工具函數:

function logErrorChain(err, level = 0) {
  if (!err) return;
  console.error(" ".repeat(level * 2) + `${err.name}: ${err.message}`);

  if (err.cause instanceof Error) {
    logErrorChain(err.cause, level + 1);
  } else if (err.cause) {
    console.error(" ".repeat((level + 1) * 2) + String(err.cause));
  }
}

如果需要完整堆棧信息:

function logFullErrorChain(err) {
  let current = err;
  while (current) {
    console.error(current.stack);
    current = current.cause instanceof Error ? current.cause : null;
  }
}

對於結構複雜、可能在不同層級出現多種故障的系統來説,這非常有用。

8. 跨層錯誤鏈示例

假設調用流程如下:

  1. 數據庫連接失敗,拋出 ConnectionTimeoutError
  2. 捕獲後包裝成 DatabaseError
  3. 再次捕獲幷包裝成 ServiceUnavailableError
class ConnectionTimeoutError extends Error {}
class DatabaseError extends Error {}
class ServiceUnavailableError extends Error {}

try {
  try {
    try {
      throw new ConnectionTimeoutError("DB connection timed out");
    } catch (networkErr) {
      throw new DatabaseError("Failed to connect to database", { cause: networkErr });
    }
  } catch (dbErr) {
    throw new ServiceUnavailableError("Unable to save user data", { cause: dbErr });
  }
} catch (finalErr) {
  logErrorChain(finalErr);
}

控制枱輸出:

ServiceUnavailableError: Unable to save user data
  DatabaseError: Failed to connect to database
    ConnectionTimeoutError: DB connection timed out

可以看到,錯誤鏈提供了一個清晰的視圖,告訴你發生了什麼以及在哪裏發生的。

9. 支持度

.cause 參數在所有現代環境中都支持:

  • ✅ Chrome 93+、Firefox 91+、Safari 15+、Edge 93+
  • ✅ Node.js 16.9+
  • ✅ Bun 和 Deno(當前版本)

需要注意的是,開發者工具可能不會自動顯示 cause。

所以需要顯式記錄它(console.error('Caused by:', err.cause))。

還要注意:如果使用 Babel 或 TypeScript 進行轉譯,此功能不會被 polyfill。

10. 異步操作中的錯誤處理

Error.cause 同樣適用於異步操作。結合 async/await 可以這樣使用:

async function fetchData() {
  try {
    const response = await fetch("/api/data");
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error("數據獲取失敗:", error);
    throw new Error("Failed to fetch data", { cause: error });
  }
}

這種方式讓異步代碼的錯誤處理邏輯看起來與同步代碼無異,大大提升了可讀性和可維護性。

11. 總結

總結一下,現代錯誤鏈處理的最佳實踐

  • 使用 new Error(message, { cause }) 保留上下文
  • 適用於內置錯誤類和自定義錯誤類
  • 所有現代運行時環境都支持(瀏覽器、Node.js、Deno、Bun)
  • 可以改善日誌、調試和測試斷言
  • 注意 TypeScript:設置 "target": "es2022""lib": ["es2022"]
  • 注意記錄 err.cause 或手動遍歷錯誤鏈

從而實現更清晰的堆棧跟蹤、更好的上下文、更愉快的調試體驗。

Error.cause 就是你錯誤處理中缺少的那一環。

12. 參考鏈接

  1. https://allthingssmitty.com/2025/11/10/error-chaining-in-javascript-cleaner-debugging-with-error-cause/
  2. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.