DEV Community

Cover image for A Hidden Gem in JavaScript Debugging: error.cause
Leapcell
Leapcell

Posted on

A Hidden Gem in JavaScript Debugging: error.cause

Cover

The Challenges of Debugging

What is the biggest challenge in debugging? One of them is undoubtedly tracing the source of errors.

Imagine this scenario:

const func = () => {
  doSth('A');
  doSth('B');
};
Enter fullscreen mode Exit fullscreen mode

When func throws an error, how do you identify at which step the error occurred? Was it caused by doSth('A'), doSth('B'), or func itself? Clearly, the error lacks sufficient context.

Common Solutions

A common approach to address this issue might look like this:

const func = () => {
  try {
    doSth('A');
  } catch (error) {
    throw new Error('An error from A', error);
  }
  try {
    doSth('B');
  } catch (error) {
    throw new Error('An error from B', error);
  }
};
Enter fullscreen mode Exit fullscreen mode

With this approach, you can locate the source of the error more easily. However, this solution has several limitations:

  1. Loss of error details:

    If the error contains extensive information (e.g., payloads, HTTP status codes, error codes), this approach only adds the error message of doSth to the newly constructed error. Other crucial details, including the original stack trace, are lost.

  2. Decreased log readability:

    With more than two potential error points, the logs can become cluttered and difficult to interpret.

  3. Ambiguity in expressing intent:

    The code does not explicitly communicate that the new error is caused by the specific doSth function being caught, leaving room for improved code readability.

Introducing error.cause

To address these issues, ECMAScript 2022 introduced error.cause.

This feature allows developers to specify the root cause of an error when creating a new error object. By using error.cause, you can establish a chain of errors, making it easier to debug and trace the root cause of an issue.

Here’s a simple example:

try {
  // Some operation that may throw an error
} catch (error) {
  throw new Error('Something went wrong', { cause: error });
}
Enter fullscreen mode Exit fullscreen mode

With this approach, you can build causal links between errors. For instance:

const func = () => {
  try {
    doSth('A');
  } catch (error) {
    throw new Error('An error from A', { cause: error });
  }
  try {
    doSth('B');
  } catch (error) {
    throw new Error('An error from B', { cause: error });
  }
};
Enter fullscreen mode Exit fullscreen mode

This allows us to catch errors thrown by lower-level functions (e.g., doSth('A')), throw a new error that adds relevant context (e.g., "An error occurred while executing doSth('A')"), and preserve the original error details (e.g., "A is an illegal argument.").

Building a Chain of Errors

Another advantage of error.cause is its ability to create a chain of linked errors, enabling developers to trace issues back through multiple layers of the application:

const func = () => {
  try {
    try {
      try {
        doSth('A');
      } catch (error) {
        throw new Error('Error at depth 3', { cause: error });
      }
    } catch (error) {
      throw new Error('Error at depth 2', { cause: error });
    }
  } catch (error) {
    throw new Error('Error at depth 1', { cause: error });
  }
};

console.log(error.cause.cause); // Error at depth 3
Enter fullscreen mode Exit fullscreen mode

In Node.js, errors with a cause are handled specially in the console. All related error stacks are printed:

const cause = new Error('The remote HTTP server responded with a 500 status');
const symptom = new Error('The message failed to send', { cause });

console.log(symptom);
// Prints:
//   Error: The message failed to send
//       at REPL2:1:17
//       at Script.runInThisContext (node:vm:130:12)
//       ... 7 lines matching cause stack trace ...
//       at [_line] [as _line] (node:internal/readline/interface:886:18) {
//     [cause]: Error: The remote HTTP server responded with a 500 status
//         at REPL1:1:15
//         at Script.runInThisContext (node:vm:130:12)
//         at REPLServer.defaultEval (node:repl:574:29)
//         at bound (node:domain:426:15)
//         at REPLServer.runBound [as eval] (node:domain:437:12)
//         at REPLServer.onLine (node:repl:902:10)
//         at REPLServer.emit (node:events:549:35)
//         at REPLServer.emit (node:domain:482:12)
//         at [_onLine] [as _onLine] (node:internal/readline/interface:425:12)
//         at [_line] [as _line] (node:internal/readline/interface:886:18)
Enter fullscreen mode Exit fullscreen mode

Conclusion

  • Debugging becomes significantly easier when you have immediate access to both the error context and details.
  • One effective way to achieve this is by adopting a "catch + rethrow with context" pattern using the error.cause feature:
try {
  doSth();
} catch (error) {
  throw new Error('Context relative to doSth', { cause: error });
}
Enter fullscreen mode Exit fullscreen mode

This approach not only improves error traceability but also enhances the readability and maintainability of your code.


We are Leapcell, your top choice for deploying Node.js projects to the cloud.

Leapcell

Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:

Multi-Language Support

  • Develop with Node.js, Python, Go, or Rust.

Deploy unlimited projects for free

  • pay only for usage β€” no requests, no charges.

Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead β€” just focus on building.

Explore more in the Documentation!

Try Leapcell

Follow us on X: @LeapcellHQ


Read on our blog

Top comments (0)