Lesson 24 of 54

Control Flow and Logic

Guard Clauses and Early Returns

Deeply nested code is hard to read. Each level of indentation is another item your brain has to hold in memory.

Guard clauses solve this by inverting the logic: instead of nesting the happy path inside validations, you reject invalid cases early and keep the happy path at the top level.

The Bouncer Pattern

Think of a nightclub bouncer. They don't escort valid customers through a maze—they check IDs at the door and reject invalid ones immediately.

Your functions should work the same way: validate at the entrance, reject bad inputs fast, then proceed with the real work.

Before: Nested Validation

Javascript
function processOrder(order) {
  if (order) {
    if (order.items && order.items.length > 0) {
      if (order.customer) {
        if (order.paymentMethod) {
          // Finally, the actual logic
          const total = calculateTotal(order);
          chargeCustomer(order.customer, order.paymentMethod, total);
          shipOrder(order);
          return { success: true };
        } else {
          return { error: 'Payment method required' };
        }
      } else {
        return { error: 'Customer required' };
      }
    } else {
      return { error: 'Order must have items' };
    }
  } else {
    return { error: 'Order required' };
  }
}

This is 4 levels deep. The actual business logic is buried. Error cases are scattered.

After: Guard Clauses

Javascript
function processOrder(order) {
  if (!order) {
    return { error: 'Order required' };
  }
  
  if (!order.items || order.items.length === 0) {
    return { error: 'Order must have items' };
  }
  
  if (!order.customer) {
    return { error: 'Customer required' };
  }
  
  if (!order.paymentMethod) {
    return { error: 'Payment method required' };
  }
  
  // Happy path at top level
  const total = calculateTotal(order);
  chargeCustomer(order.customer, order.paymentMethod, total);
  shipOrder(order);
  return { success: true };
}

Same logic. Flat structure. Easy to follow.

How Guard Clauses Work

A guard clause is a conditional that returns early when a precondition isn't met.

Pattern:

Javascript
if (invalidCondition) {
  return errorResult;
}
// Continue with valid case

The key insight: each guard eliminates a case from consideration. After the guard, you know that case is impossible.

Javascript
function withdraw(account, amount) {
  // After this line, we know account exists
  if (!account) throw new Error('Account required');
  
  // After this line, we know amount is positive
  if (amount <= 0) throw new Error('Amount must be positive');
  
  // After this line, we know there's enough balance
  if (account.balance < amount) throw new Error('Insufficient funds');
  
  // Now we can safely proceed
  account.balance -= amount;
  return account.balance;
}

Types of Guards

Null/Undefined Checks

Javascript
function processUser(user) {
  if (!user) return null;
  // user is definitely defined here
}

Validation Checks

Javascript
function sendEmail(to, subject, body) {
  if (!isValidEmail(to)) throw new InvalidEmailError(to);
  if (!subject.trim()) throw new Error('Subject required');
  if (!body.trim()) throw new Error('Body required');
  // All inputs are valid
}

Permission Checks

Javascript
async function deleteDocument(documentId, user) {
  if (!user.isAuthenticated) throw new AuthenticationError();
  if (!user.hasPermission('delete')) throw new AuthorizationError();
  // User is authorized
}

State Checks

Javascript
function submitOrder(order) {
  if (order.status !== 'draft') {
    throw new Error('Can only submit draft orders');
  }
  // Order is in correct state
}

Guard Clause vs. If-Else

Guard clauses are best when:

  • You're checking preconditions
  • Invalid cases should return early
  • The happy path is the main logic

Traditional if-else is best when:

  • Both branches have equally important logic
  • You're choosing between alternatives, not validating
Javascript
// GUARD CLAUSE: Checking preconditions
function processPayment(payment) {
  if (!payment) return { error: 'No payment' };
  if (payment.amount <= 0) return { error: 'Invalid amount' };
  
  // Process the valid payment
  return charge(payment);
}

// IF-ELSE: Choosing between equal alternatives
function getPricing(customerType) {
  if (customerType === 'enterprise') {
    return calculateEnterprisePricing();
  } else {
    return calculateStandardPricing();
  }
}

Extracting Complex Guards

When guards become complex, extract them:

Javascript
// Before: Complex inline guard
function approveExpense(expense, approver) {
  if (
    expense.amount > 10000 ||
    expense.category === 'travel' && expense.amount > 5000 ||
    !approver.canApprove(expense.department)
  ) {
    return { error: 'Cannot approve' };
  }
  // ...
}

// After: Extracted guard function
function approveExpense(expense, approver) {
  if (!canApprove(expense, approver)) {
    return { error: 'Cannot approve' };
  }
  // ...
}

function canApprove(expense, approver) {
  if (expense.amount > 10000) return false;
  if (expense.category === 'travel' && expense.amount > 5000) return false;
  if (!approver.canApprove(expense.department)) return false;
  return true;
}

Combining Guards with TypeScript

TypeScript's type narrowing works naturally with guard clauses:

Typescript
function processOrder(order: Order | null): OrderResult {
  if (!order) {
    return { error: 'No order' };
  }
  // TypeScript knows order is Order, not null
  
  if (!order.items.length) {
    return { error: 'No items' };
  }
  // TypeScript knows order.items is non-empty
  
  return process(order);
}

Each guard narrows the type, making subsequent code type-safe.

The "Early Return" Debate

Some developers were taught "one return per function." This leads to code like:

Javascript
function calculate(input) {
  let result;
  if (input) {
    if (input.valid) {
      result = input.value * 2;
    } else {
      result = 0;
    }
  } else {
    result = -1;
  }
  return result;
}

Multiple early returns are clearer:

Javascript
function calculate(input) {
  if (!input) return -1;
  if (!input.valid) return 0;
  return input.value * 2;
}

The "single return" rule made sense in C when you needed to clean up resources. Modern languages with garbage collection and try/finally don't have this constraint.


Key insight: Guard clauses reduce nesting by handling error cases early. Each guard eliminates an invalid case, leaving only the valid case for the main logic. Think like a bouncer: check at the door, reject immediately, then let the valid cases through.