Simplifying Conditional Expressions
Conditionals are where bugs hide.
Every if, else, and switch is a branch in your code. Each branch doubles the paths through your function. Three conditionals? Eight paths. Five conditionals? Thirty-two paths.
Simplifying conditionals is about reducing cognitive load—making it obvious which path executes and why.
Positive Over Negative
The human brain processes positive statements faster than negative ones.
// BAD: Double negative
if (!isNotActive) { }
// BAD: Negative condition
if (!user.isInactive) { }
// GOOD: Positive condition
if (user.isActive) { }
When you see !isInactive, you have to mentally translate: "not inactive means... active."
When you see isActive, you understand immediately.
Reframe Your Booleans
Instead of:
isNotFound→isFound(flip the logic)hasNoErrors→hasErrorsorisValidisDisabled→isEnabled(if enabled is the normal state)!isEmpty→hasItems
// Before
if (!user.isNotVerified && !cart.isEmpty) {
checkout();
}
// After
if (user.isVerified && cart.hasItems) {
checkout();
}
Handle the Common Case First
Put the most likely path first. This reduces cognitive load because readers encounter the expected behavior immediately.
// BAD: Error case first
function getUser(id) {
if (!id) {
throw new Error('ID required');
} else {
return db.users.find(id);
}
}
// GOOD: Happy path first
function getUser(id) {
if (id) {
return db.users.find(id);
}
throw new Error('ID required');
}
// BETTER: Guard clause (covered next lesson)
function getUser(id) {
if (!id) throw new Error('ID required');
return db.users.find(id);
}
The Exception: Guard Clauses
Guard clauses (early returns for error cases) are an exception to "common case first." We'll cover these in the next lesson. They work because they eliminate the error cases early, leaving only the happy path.
Extract Complex Conditions
When a condition is complex, give it a name.
Before: Inline Complexity
if (
user.subscriptionType === 'premium' &&
user.accountAge > 365 &&
!user.hasPaymentIssues &&
user.region !== 'restricted' &&
order.total > 100
) {
applyPremiumDiscount(order);
}
You have to read all five conditions to understand when the discount applies.
After: Named Condition
const isEligibleForPremiumDiscount =
user.subscriptionType === 'premium' &&
user.accountAge > 365 &&
!user.hasPaymentIssues &&
user.region !== 'restricted' &&
order.total > 100;
if (isEligibleForPremiumDiscount) {
applyPremiumDiscount(order);
}
Now the if statement is readable at a glance.
Even Better: Extract to Function
if (isEligibleForPremiumDiscount(user, order)) {
applyPremiumDiscount(order);
}
function isEligibleForPremiumDiscount(user, order) {
if (user.subscriptionType !== 'premium') return false;
if (user.accountAge <= 365) return false;
if (user.hasPaymentIssues) return false;
if (user.region === 'restricted') return false;
if (order.total <= 100) return false;
return true;
}
Now:
- The calling code is trivially simple
- The eligibility rules are documented by the function name
- Each rule is on its own line, easy to understand and modify
- Adding or removing rules is straightforward
Simplify Nested Conditionals
Nested conditionals multiply complexity.
Before: Nested
if (user) {
if (user.isActive) {
if (user.hasPermission('edit')) {
return 'Can edit';
} else {
return 'No edit permission';
}
} else {
return 'User inactive';
}
} else {
return 'No user';
}
Five paths. Hard to trace which returns when.
After: Flattened
if (!user) return 'No user';
if (!user.isActive) return 'User inactive';
if (!user.hasPermission('edit')) return 'No edit permission';
return 'Can edit';
Same logic. Four lines. Each condition is clear.
Use Lookup Tables
When you have many similar conditions, a lookup table is often clearer than if/else chains.
Before: If/Else Chain
function getStatusMessage(status) {
if (status === 'pending') {
return 'Your order is being processed';
} else if (status === 'shipped') {
return 'Your order is on its way';
} else if (status === 'delivered') {
return 'Your order has been delivered';
} else if (status === 'cancelled') {
return 'Your order was cancelled';
} else {
return 'Unknown status';
}
}
After: Lookup Table
const STATUS_MESSAGES = {
pending: 'Your order is being processed',
shipped: 'Your order is on its way',
delivered: 'Your order has been delivered',
cancelled: 'Your order was cancelled',
};
function getStatusMessage(status) {
return STATUS_MESSAGES[status] ?? 'Unknown status';
}
The table is scannable. Adding a new status means adding one line.
The Ternary Trap
Ternary operators are great for simple conditions:
const greeting = isLoggedIn ? 'Welcome back' : 'Please log in';
They're terrible for complex ones:
// DON'T
const price = isPreferred
? hasDiscount
? originalPrice * 0.8
: originalPrice * 0.9
: hasDiscount
? originalPrice * 0.95
: originalPrice;
// DO
function calculatePrice(originalPrice, isPreferred, hasDiscount) {
if (isPreferred && hasDiscount) return originalPrice * 0.8;
if (isPreferred) return originalPrice * 0.9;
if (hasDiscount) return originalPrice * 0.95;
return originalPrice;
}
Rule of thumb: if a ternary needs nesting, use if/else.
Key insight: Conditionals should be immediately clear. Use positive conditions, put common cases first, extract complex logic into named variables or functions, and prefer lookup tables over long if/else chains.