[Issue 3613] Error Chaining in JavaScript: Using Error.cause for Clearer Debugging

[Issue 3613] Error Chaining in JavaScript: Using Error.cause for Clearer Debugging
image

Preface

This article explains how to use JavaScript's `Error.cause` property and why it’s valuable for clearer, more precise error handling.

In JavaScript, throwing errors is easy — but understanding the real root cause is often messy. The `cause` property solves this by letting us chain errors while preserving their original context.

📎 Related reading: Issue #3532 – The Evolution of Error Pages

---

Problems with Traditional Error Handling

In multilayered code — services calling services, wrapped functions, bubbled errors — it’s common to lose track of where the failure started.

Example: Old Approach

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

Here, once you wrap the original error, you lose:

  • The original stack trace
  • The specific error type

---

Introducing `Error.cause`

With the `cause` parameter, you can wrap errors without losing their original details:

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

Output:

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

✅ The top-level message stays clear,

✅ The root cause remains intact.

📎 Related: Issue #3479 – Guidelines for Error Messages in UI Design

---

Practical Example

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 object
  console.error(err.cause instanceof SyntaxError); // true
}

Key Points

  • The `cause` property is non-enumerable — it won’t clutter your logs unless you explicitly print it.
  • Similar behavior to `message` and `stack`.

💡 Tip: Stack traces are not automatically combined — you must manually log `err.cause.stack`.

---

Before `cause`: Workarounds

Before ES2022, developers had to:

  • Concat strings into `message`
  • Add custom `.originalError`
  • Fully wrap errors

These were inconsistent and often overwrote metadata.

📎 Related: 【Issue 3451】Permanent Fix for Frontend `TypeError`

---

Using `cause` in Custom Errors

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

For TypeScript:

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

Without this, you may get type errors when passing `{ cause }`.

---

Better Test Assertions

Error chains make tests clearer:

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

---

Caveats & Best Practices

  • `console.error(err)` shows only the top-level error — log causes manually.
  • Use `cause` sparingly — wrap errors only for meaningful context.

---

Utility: Recursively Log Error Chains

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

---

Logging the Full Stack Chain

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

---

Cross-Layer Error Chain Example

Example chain:

  • ConnectionTimeoutError — DB connection failed
  • DatabaseError — wraps connection error
  • ServiceUnavailableError — wraps database error
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);
}

Console:

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

---

Browser & Runtime Support

  • Browsers: Chrome 93+, Firefox 91+, Safari 15+, Edge 93+
  • Node.js: 16.9+
  • Others: Bun & Deno (latest)

> ⚠️ DevTools may not display `cause` automatically — log it manually.

> Babel/TypeScript will not polyfill `cause`.

---

Summary

✅ What `Error.cause` Gives You

  • Preserve context with `new Error(message, { cause })`
  • Works with built-in & custom errors
  • Supported across modern runtimes
  • Clearer logs, better debugging, stronger tests

⚠️ Remember:

  • Manually log `err.cause`
  • Avoid chaining trivial errors

---

For developers sharing knowledge like this, AiToEarn官网 enables:

  • AI-powered content generation
  • Publishing to multiple platforms automatically
  • Analytics & monetization opportunities

Also see: AiToEarn博客 | AiToEarn文档 | AI模型排名

---

About This Article

Translation of:

https://allthingssmitty.com/2025/11/10/error-chaining-in-javascript-cleaner-debugging-with-error-cause/

Read more

Translate the following blog post title into English, concise and natural. Return plain text only without quotes. 哈佛大学 R 编程课程介绍

Harvard CS50: Introduction to Programming with R Harvard University offers exceptional beginner-friendly computer science courses. We’re excited to announce the release of Harvard CS50’s Introduction to Programming in R, a powerful language widely used for statistical computing, data science, and graphics. This course was developed by Carter Zenke.