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)
119: Defensive Programming Techniques
Look, we've all been there. You write a function, it works perfectly with the test data you created, and you push it to production feeling great. Then, three weeks later, a user with a corrupted profile or an unexpected API response triggers a TypeError: Cannot read property 'x' of undefined, and suddenly you're debugging a production crash at 2:00 AM. That's where defensive programming comes in.
Defensive programming isn't about being paranoid; it's about accepting that the world is messy. Your inputs will be wrong, your network will fail, and other developers (including your future self) will pass the wrong types into your functions. The goal is to make your code fail gracefully rather than crashing the entire application.
The Danger of Trusting Your Input
Let's look at a common scenario. Imagine you're writing a function to calculate how many years a user has been a member of a service. In a "happy path" mindset, you assume the user object is always there and the date is always valid.
function getMembershipYears(user) {
const joinDate = new Date(user.joinedAt);
const currentYear = new Date().getFullYear();
return currentYear - joinDate.getFullYear();
}
To a junior dev, this looks clean. To an experienced engineer, this is a landmine. What happens if user is null? The app crashes. What if user.joinedAt is missing? new Date(undefined) creates an "Invalid Date" object, and getFullYear() will return NaN. Your UI then displays "You have been a member for NaN years," which looks unprofessional at best and breaks layout logic at worst. You've trusted the data source too much.
Guard Clauses and the Art of Early Returns
The better approach is to assume the input is potentially malicious or broken. Instead of wrapping your entire logic in a giant if/else block—which creates a "pyramid of doom" indentation—I prefer using guard clauses. These are checks at the top of your function that "bail out" early if the conditions aren't met.
function getMembershipYears(user) {
// Guard 1: Is the user object even there?
if (!user || typeof user !== 'object') {
console.warn('getMembershipYears was called without a valid user object');
return 0;
}
// Guard 2: Does the specific property exist?
if (!user.joinedAt) {
return 0;
}
const joinDate = new Date(user.joinedAt);
// Guard 3: Is the date actually valid?
if (isNaN(joinDate.getTime())) {
console.error(`Invalid date format for user ${user.id}: ${user.joinedAt}`);
return 0;
}
const currentYear = new Date().getFullYear();
return currentYear - joinDate.getFullYear();
}
By checking for the "bad" states first, the actual business logic stays flat and easy to read at the bottom of the function. I've also added some logging. When you're coding defensively, you don't just want to stop the crash; you want to leave a breadcrumb trail so you can actually fix the root cause of the bad data later.
Balancing Robustness with Readability
Now, you might be thinking, "If I do this for every single function, my codebase will double in size." You're not wrong. There is a trade-off here. If you over-engineer every single internal helper function with ten guard clauses, you'll end up with a codebase that's hard to navigate and feels bloated.
The trick is knowing where to draw the line. I apply the strictest defensive techniques at the "boundaries" of my application: API response handlers, user input forms, and public-facing library functions. If I'm calling a private helper function inside a class where I completely control the data flow, I might be less strict. However, when in doubt, lean toward safety. It's much easier to remove a redundant check later than it is to track down a random null pointer exception in a production log with ten thousand entries.
📋 Practical Task
Hardening the Payment Processing Pipeline
You have been handed a fragile function used in a checkout system. It is currently causing intermittent crashes because some payment gateways send slightly different data structures than others.
Your Task: Rewrite the processPayment function using defensive programming techniques. Implement guard clauses to ensure the function doesn't crash if the payment object is missing, if the amount is not a positive number, or if the currency is not a string.
// FRAGILE CODE - Fix this!
function processPayment(payment) {
const taxRate = 0.08;
const total = payment.amount + (payment.amount * taxRate);
console.log(`Processing ${payment.currency} ${total}...`);
return total;
}
// Test cases that currently break the function:
processPayment(null);
processPayment({ amount: "100", currency: "USD" }); // amount is a string
processPayment({ amount: -50, currency: "USD" }); // negative amount
processPayment({ amount: 100 }); // missing currency
Your solution should return null or 0 and log a descriptive warning message whenever a guard clause is triggered, ensuring the program continues to run without throwing an uncaught exception.
There are no comments for now.