阅读视图

发现新文章,点击刷新页面。

涨见识了,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. allthingssmitty.com/2025/11/10/…
  2. developer.mozilla.org/zh-CN/docs/…
❌