Error.cause — Error Chaining
debt(d7/e1/b3/t5)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints.tools list is empty. The common mistakes — forgetting to pass { cause: e }, setting cause to a string, or logging only err.message — are not caught by syntax checks or default linters. A developer must manually review re-throw sites or notice missing context in production logs. No standard static analysis rule flags missing cause in catch blocks, so this lives firmly in code-review territory.
Closest to 'one-line patch or single-call swap' (e1). The quick_fix is explicit: adding '{ cause: e }' to an existing throw statement is a single-argument addition per re-throw site. Each fix is one line; while multiple sites may exist, each individual correction is trivial.
Closest to 'localised tax' (b3). The applies_to scope is web and cli, which is broad, but the concept is opt-in per catch block rather than a load-bearing architectural choice. It imposes a discipline tax — developers must remember the pattern at every re-throw site — but it doesn't shape the system's architecture or slow down unrelated work streams. The logging/serialisation concerns (toJSON, serialize-error) add a small persistent cost in error-handling infrastructure.
Closest to 'notable trap (a documented gotcha most devs eventually learn)' (t5). The misconception field calls out that Error.cause is NOT automatically displayed in console.error() across all environments — browsers may silently omit it. Combined with the common mistake of forgetting { cause: e } entirely (silently losing stack context) and the silent no-op on Node.js < 16.9, these are documented gotchas that developers discover the hard way but are not catastrophically counter-intuitive to the concept's name.
Also Known As
TL;DR
Explanation
Before error cause, rethrowing with context required storing the original error separately or appending its message to the new one, losing the original stack trace. Error cause standardises this: any Error constructor accepts a second options object with a cause property. The cause can be any value — another Error, a string, an object. It is accessible as err.cause. Error.cause enables proper error chains: a network error causes a fetch error causes an authentication error. Logging and monitoring tools (Sentry, Datadog) surface the full chain. Node.js 16.9+ and all modern browsers support it.
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Not passing { cause: e } when rethrowing — the most common mistake; catching and throwing a new Error without cause loses the original stack entirely.
- Setting cause to a string instead of an Error — err.cause = 'some string' loses the stack trace; always use the original Error object as cause.
- Not checking for cause support in old Node.js — Error.cause requires Node.js 16.9+; on older versions the option is silently ignored.
- Logging only err.message in error handlers — always log err.cause as well, or use a logging library that serialises the full cause chain.
- Losing the cause through JSON.stringify — cause is not serialised by default; implement toJSON() on your Error subclass or use a library like serialize-error.
Avoid When
- When the original error is already logged or will never be inspected — wrapping adds noise without benefit.
- When chaining deeply (more than 3–4 levels) — the chain becomes hard to traverse and obscures the root cause.
- When the cause is user-facing or sensitive data that should not leak into error logs or monitoring systems.
- When building a cause from a non-Error value (string, number) without validation — the cause becomes ambiguous and hard to inspect downstream.
When To Use
- Wrap low-level errors (network, database, file system) in domain-specific errors without losing the original stack trace and debugging context.
- Build error chains across abstraction layers (API client wraps HTTP errors, service layer wraps client errors, controller wraps service errors) so monitoring tools and logs show the full causal chain.
- Distinguish between error types at different levels (distinguish 'DatabaseConnectionError caused by timeout' from 'DatabaseConnectionError caused by authentication failure') for targeted error handling and recovery logic.
- Preserve original Error objects in async/await code where errors pass through multiple try-catch blocks, ensuring the root cause remains inspectable for production debugging and incident analysis.
Code Examples
// ❌ Losing the original error — only the new message survives
async function loadUser(id) {
try {
const resp = await fetch(`/api/users/${id}`);
return await resp.json();
} catch (e) {
// Original error (e.g. NetworkError, SyntaxError) is thrown away
throw new Error(`Failed to load user ${id}`);
}
}
// ❌ Appending message — ugly, loses stack trace
} catch (e) {
throw new Error(`Failed to load user ${id}: ${e.message}`);
}
// ✅ Error.cause — full chain preserved
async function loadUser(id) {
try {
const resp = await fetch(`/api/users/${id}`);
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}`, { cause: resp });
}
return await resp.json();
} catch (e) {
throw new Error(`Failed to load user ${id}`, { cause: e });
}
}
// Error handler sees the full chain
try {
await loadUser(42);
} catch (e) {
console.error(e.message); // 'Failed to load user 42'
console.error(e.cause.message); // 'HTTP 404'
// Sentry / Datadog get the full chain automatically
}
// Traversing the cause chain
function getRootCause(err) {
return err.cause ? getRootCause(err.cause) : err;
}