JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
238: Error.cause for Error Chaining
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
tryblock, attempt to runJSON.parse(configString). - In the
catchblock, throw a newError("AppInitializationError: The application failed to start"). - Use the
causeproperty to attach the originalSyntaxErrorfromJSON.parseto your new error. - Call
initializeApp("{ invalid json }")and catch the resulting error. Log both the main error message and the message of thecauseto the console.
There are no comments for now.