Lesson 23 of 54

Control Flow and Logic

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.

Javascript
// 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:

  • isNotFoundisFound (flip the logic)
  • hasNoErrorshasErrors or isValid
  • isDisabledisEnabled (if enabled is the normal state)
  • !isEmptyhasItems
Javascript
// 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.

Javascript
// 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

Javascript
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

Javascript
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

Javascript
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

Javascript
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

Javascript
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

Javascript
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

Javascript
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:

Javascript
const greeting = isLoggedIn ? 'Welcome back' : 'Please log in';

They're terrible for complex ones:

Javascript
// 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.