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');
};
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);
}
};
With this approach, you can locate the source of the error more easily. However, this solution has several limitations:
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 ofdoSth
to the newly constructed error. Other crucial details, including the original stack trace, are lost.Decreased log readability:
With more than two potential error points, the logs can become cluttered and difficult to interpret.Ambiguity in expressing intent:
The code does not explicitly communicate that the new error is caused by the specificdoSth
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 });
}
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 });
}
};
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
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)
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 });
}
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 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!
Follow us on X: @LeapcellHQ
Top comments (0)