JavaScript中的错误链:借助Error.cause实现更清晰的调试

JavaScript的错误处理始终显得有些混乱。抛出错误很简单,但要追溯到根本原因?就没那么容易了。这就是cause属性的用武之地。

传统错误处理的困境

当处理分层代码时(例如服务调用服务、封装函数、错误冒泡等),很容易迷失 真正 出错的位置。传统做法可能如下所示:

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

虽然封装了错误,但原始堆栈跟踪和错误类型已丢失。

元素周期表

引入 Error.cause

通过使用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.cause 时的效果如下(注意可同时访问两个堆栈跟踪):

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

现在您既能保留原始错误,又能呈现清晰的顶级消息。

实际应用示例

function fetchUserData() {
  try {
    JSON.parse('{ broken: true }'); // ← This will fail
  } 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
}

这相当巧妙。

通过Error构造函数传递时,cause属性在规范中被定义为 不可枚举 ,因此除非显式访问,否则不会污染日志或for...in循环。(这与messagestack的行为一致。)

⚠️ 注意 :JavaScript不会自动合并堆栈跟踪。新错误的堆栈跟踪是独立存在的。要查看完整信息,需手动检查 err.cause.stack

cause出现前的权宜之计

在引入 cause 属性(ES2022)之前,开发者只能依赖不稳定的临时方案:字符串拼接、自定义 .originalError 属性,或完全封装错误对象。这些方法会覆盖原始堆栈跟踪或错误类型等关键元数据。

cause 属性以规范化的方式彻底解决了此问题。

自定义错误同样适用

您也可在自定义错误类中使用 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"]
  }
}

否则向 Error 构造函数传递 { cause } 时可能出现类型错误。

更优的测试断言

错误链不仅在运行时有用,在测试中同样能发挥作用。

假设你的服务抛出由ValidationError引发的UserCreationError。此时不必仅检查顶级错误,可通过断言实现:

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

这能让测试更清晰、更健壮。

注意事项与最佳实践

默认情况下,console.error(err) 仅记录顶级错误。cause 链不会自动显示,需手动记录:

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

切忌过度使用。将每个次要错误都进行链式记录反而会使调试过程更加混乱。仅在上下文真正重要时使用此功能。

递归记录完整错误链

以下是一个安全遍历链的小工具:

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;
  }
}

特别适用于多层级系统,能有效捕捉不同层级同时发生的错误。

跨层级错误链

设想以下流程:

  • 数据库调用因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

错误链让你清晰了解发生了什么……以及 发生的位置

浏览器与运行时支持

.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 支持。

📌 更现代的模式

若您追求更简洁的异步代码,Array.fromAsync() 将带来革命性体验。

现代错误链式处理

  • ✅ 使用 new Error(message, { cause }) 保留上下文
  • ✅ 兼容内置与自定义错误类
  • ✅ 支持所有现代运行时环境(浏览器、Node、Deno、Bun)
  • ✅ 优化日志记录、调试和测试断言
  • ✅ TypeScript:设置 “target”: “es2022”‘lib’: [“es2022”]
  • ⚠️ 切记记录 err.cause 或手动遍历错误链

更清晰的堆栈跟踪。更丰富的上下文。更愉悦的调试体验。

你也许感兴趣的:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注