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)
236: Creating Custom Error Subclasses
A few years ago, I was reviewing a pull request for a payment processing module. The developer had wrapped everything in a massive try-catch block, and every single failure—whether it was a timed-out API call, an expired credit card, or a database deadlock—was thrown as a generic Error. When it hit the catch block, the code just logged "An error occurred" and returned a 500 status. We spent three days digging through logs just to figure out that a specific third-party vendor was rejecting a specific currency code. If we had been throwing specific error types, the catch block could have instantly identified the "CurrencyNotSupportedError" and responded with a helpful 400 Bad Request instead of a generic crash.
The Power of Type-Checking Your Failures
When you throw a basic new Error('Something went wrong'), you're essentially throwing a string with a stack trace. The problem is that once that error lands in a catch block, you have no programmatic way to distinguish it from any other error except by parsing the error message string—which is a fragile nightmare because one typo in a string literal can break your entire error-handling logic.
By creating a subclass of the built-in Error class, you give your errors a "type." This allows you to use the instanceof operator to handle different failure scenarios gracefully. I always tell my team: don't just tell the system that something failed; tell the system what failed.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
try {
throw new ValidationError("Invalid email format");
} catch (err) {
if (err instanceof ValidationError) {
console.log("User input error: " + err.message);
} else {
console.log("Unexpected system error: " + err.message);
}
}
Cleaning Up the Prototype and Name
You'll notice in the example above that I manually set this.name = "ValidationError". If you skip this, the error will still function, but when you log it to the console, it will just say "Error: Invalid email format" instead of "ValidationError: Invalid email format". That small detail saves you an immense amount of time when scanning through a thousand lines of production logs.
In some older environments or specific transpilation setups, you might find that the prototype chain gets a bit wonky when extending built-in classes. While modern JavaScript handles this well, you might occasionally see developers manually setting the prototype. For 99% of modern projects, simply calling super(message) and assigning the name property is all you need to keep your stack traces intact and your debugging sanity preserved.
I usually recommend creating a dedicated errors.js file in your project. Instead of scattering custom classes throughout your business logic, define all your domain-specific errors in one place. This makes it incredibly easy for other developers on your team to see exactly what failure modes they need to account for when calling your functions.
📋 Practical Task
Build a Specialized User Authentication Error System
You are building a login system that needs to distinguish between different types of authentication failures so the UI can show specific messages (e.g., "Wrong password" vs "Account locked").
Your Task:
- Create a base class called
AuthErrorthat extends the built-inErrorclass. - Create two subclasses that extend
AuthError:InvalidCredentialsErrorandAccountLockedError. - Ensure each subclass correctly sets its
nameproperty to match the class name. - Write a function
authenticate(user)that:- Throws an
InvalidCredentialsErrorifuser.passwordis not "secret123". - Throws an
AccountLockedErrorifuser.isLockedis true.
- Throws an
- Wrap the function call in a try-catch block and use
instanceofto log a different message for each specific error type, and a generic message for any other unexpected errors.
There are no comments for now.