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)
115: Try/Catch and Custom Error Classes
We've all been there: you're staring at a production log and you see Error: Invalid input. It tells you that something failed, but it doesn't tell you what. Was the input missing? Was it the wrong type? Did a database constraint trigger it? When you rely on generic errors, you're essentially throwing a flare into the sky and hoping the person who finds it knows exactly where you're stranded.
The Trap of the Generic Error
The naive way to handle failures is to wrap everything in a try/catch block and throw the built-in Error object. It feels efficient at first because it's fast to write. Let's look at a hypothetical payment processing function where we just throw generic errors for everything that goes sideways.
async function processPayment(amount, account) {
if (amount <= 0) {
throw new Error("Invalid amount");
}
if (account.balance < amount) {
throw new Error("Insufficient funds");
}
// Imagine a network call here that might fail
const success = await bankApi.charge(account, amount);
if (!success) {
throw new Error("Bank API failure");
}
}
try {
await processPayment(100, userAccount);
} catch (e) {
console.log("Payment failed: " + e.message);
}
This works for a simple script, but it's a nightmare for a real application. Why? Because the catch block has no idea why the payment failed. If you want to show the user a "Please add more funds" message for insufficient balance, but a "Try again later" message for a bank API failure, you're forced to do something incredibly brittle: you have to check the error message string. I've seen developers write if (e.message === "Insufficient funds") in professional codebases, and it's a recipe for disaster. One typo in the string or a small wording change in the function, and your error handling logic silently breaks.
Categorizing Failures with Custom Classes
The better approach is to treat errors as data. In JavaScript, we can do this by extending the native Error class. By creating our own error types, we move the logic from "what does the message say?" to "what kind of error is this?".
class PaymentError extends Error {
constructor(message) {
super(message);
this.name = "PaymentError";
}
}
class InsufficientFundsError extends PaymentError {
constructor(balance, attempt) {
super(`Balance of ${balance} is too low for request of ${attempt}`);
this.name = "InsufficientFundsError";
this.balance = balance;
this.attempt = attempt;
}
}
class BankApiError extends PaymentError {
constructor(statusCode) {
super(`Bank API returned status ${statusCode}`);
this.name = "BankApiError";
this.statusCode = statusCode;
}
}
Notice how InsufficientFundsError does more than just hold a message; it holds the actual data (the balance and the attempt). This is where the real power lies. You're not just logging a string; you're passing a structured object that the rest of your app can use to make decisions.
Filtering the Noise in the Catch Block
Now, when we implement the try/catch, we can use the instanceof operator. This allows us to handle specific failures gracefully while letting unexpected system crashes (like a ReferenceError or TypeError) bubble up to a global logger where they belong.
try {
await processPayment(100, userAccount);
} catch (e) {
if (e instanceof InsufficientFundsError) {
showUserTopUpModal(e.balance);
} else if (e instanceof BankApiError) {
scheduleRetry(e.statusCode);
} else if (e instanceof PaymentError) {
showGenericPaymentError(e.message);
} else {
// This is a real bug, not a business logic error
reportToSentry(e);
showCriticalErrorPage();
}
}
The trade-off here is a bit more boilerplate. You have to write these classes upfront. But the cost is negligible compared to the time you'll save when you don't have to hunt through thousands of lines of logs to figure out why a user's checkout failed. You've turned your errors from "unfortunate accidents" into "documented business outcomes."
📋 Practical Task
Implementing a Tiered Validation System for a Subscription Service
You are building a subscription upgrade system. The system must validate three things: the user's current plan, their payment method validity, and whether the requested plan is actually available in their region.
Your task:
- Create a base class
SubscriptionErrorthat extendsError. - Create three specific error classes:
PlanIncompatibleError,ExpiredPaymentError, andRegionalRestrictionError, all extendingSubscriptionError. - The
RegionalRestrictionErrorshould accept aregionproperty in its constructor to store which region caused the failure. - Write a function
upgradeSubscription(user, targetPlan)that throws these specific errors based on the following conditions:- If
user.plan === targetPlan, throwPlanIncompatibleError. - If
user.paymentMethod.expired === true, throwExpiredPaymentError. - If
targetPlan.restrictedRegions.includes(user.region), throwRegionalRestrictionError.
- If
- Implement a
try/catchblock that callsupgradeSubscription. Inside thecatch, useinstanceofto log a custom message for each error type. If the error is aRegionalRestrictionError, the log must include the specific region stored in the error object.
There are no comments for now.