Skip to Content
Course content

238: Error.cause for Error Chaining

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time debugging production logs where the only thing I see is a generic error message like "Database Operation Failed". It's incredibly frustrating because, while that tells me *what* happened, it completely hides *why* it happened. The original exception—the one that actually knows about the timeout or the syntax error—gets swallowed and disappears into the void.

The vanishing stack trace

Let me show you what I mean. Imagine we're writing a function to fetch a user's settings from a remote API. We want to wrap the low-level network error in a more descriptive "domain" error so the rest of the app knows how to handle it.

async function loadUserSettings(userId) {
  try {
    const response = await fetch(`/api/settings/${userId}`);
    const data = await response.json(); // Imagine this fails because of a 500 error returning HTML
    return data;
  } catch (err) {
    // I want to provide more context, so I throw a new Error
    throw new Error(`Could not load settings for user ${userId}`);
  }
}

loadUserSettings(123).catch(err => console.log(err.stack));

When I run this, the console tells me "Error: Could not load settings for user 123". But the original error—the SyntaxError from response.json()—is gone. It's not in the stack trace. I've effectively deleted the most useful piece of forensic evidence I had.

Trying a manual workaround

In the past, I used to handle this by manually attaching the original error to a custom property. I'd do something like newError.originalError = err. It works, but it's not standardized. If I'm passing this error to a logging library or a third-party monitoring tool, that tool doesn't know to look for a property called originalError. It just sees the top-level error.

This is why Error.cause was added to the language. It gives us a standardized way to "chain" errors together.

Connecting the dots with .cause

Let's adjust that loadUserSettings function. Instead of just passing a string to the Error constructor, we can pass an options object as the second argument. This object accepts a cause property.

async function loadUserSettings(userId) {
  try {
    const response = await fetch(`/api/settings/${userId}`);
    const data = await response.json();
    return data;
  } catch (err) {
    // Now we pass the original error as the 'cause'
    throw new Error(`Could not load settings for user ${userId}`, { cause: err });
  }
}

loadUserSettings(123).catch(err => {
  console.log("Message:", err.message);
  console.log("Cause:", err.cause);
});

Now, when this fails, err.message still gives us the high-level context ("Could not load settings..."), but err.cause holds the actual SyntaxError. If you log the entire error object in a modern browser console, you'll see it neatly nested. It tells you "Error: ... caused by SyntaxError: ...".

Why this actually matters in the wild

You might be wondering, "Why not just log the original error and throw the new one?" Because in a large system, the code that catches the error is often far away from the code that threw it. By using cause, you create a breadcrumb trail. You can wrap a low-level network error in a "Repository Error," then wrap that in a "Use Case Error," and finally wrap that in a "Controller Error."

When the error finally hits your logger, you don't just have one message; you have a full history of how the failure propagated through your architecture. It turns a "something went wrong" mystery into a "this specific line in the parser failed, which broke the repository, which broke the user profile page" roadmap.




📋 Practical Task

Implementing an Error Chain for a Configuration Parser

You are building a system that loads a JSON configuration file. If the file is missing, it's a FileNotFound issue. If the JSON is malformed, it's a ParsingError. However, the application needs to report these as a high-level AppInitializationError so the UI can show a "Critical Setup Failure" screen.

Your Task:

  • Create a function initializeApp(configString).
  • Inside a try block, attempt to run JSON.parse(configString).
  • In the catch block, throw a new Error("AppInitializationError: The application failed to start").
  • Use the cause property to attach the original SyntaxError from JSON.parse to your new error.
  • Call initializeApp("{ invalid json }") and catch the resulting error. Log both the main error message and the message of the cause to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.