涨见识了,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. 跨层错误链示例
假设调用流程如下:
- 数据库连接失败,抛出
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 就是你错误处理中缺少的那一环。