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. 跨層錯誤鏈示例
假設調用流程如下:
- 數據庫連接失敗,拋出
ConnectionTimeoutError - 捕獲後包裝成
DatabaseError - 再次捕獲幷包裝成
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. 參考鏈接
- https://allthingssmitty.com/2025/11/10/error-chaining-in-javascript-cleaner-debugging-with-error-cause/
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error/cause