Category: Programming

  • The Try Pattern

    The Try Pattern

    What I Learned About Return Types During a Code Review

    Today I was doing a code review on a state management class I’d been working on. Everything looked fine at first, clean separation of concerns, good type safety, proper encapsulation. But then I noticed something odd about one of my methods:

    public handleUserAction(action: UserAction) {
      if (this.lifecycleState !== 'Active') return;
      
      const nextState = this.transition(this.currentState, action);
      if (!nextState) return;
      
      this.currentState = nextState;
      this.notify();
    }

    No return type specified. Just… void by default.

    I paused. Should this return something? I wasn’t sure, so I asked my AI Assistant. What I learned completely changed how I think about API design.

    The Problem with void Returns

    Consider this common scenario:

    public handleUserAction(action: UserAction): void {
      if (this.lifecycleState !== 'Active') return;
      
      const nextState = this.transition(this.currentState, action);
      if (!nextState) return;
      
      this.currentState = nextState;
      this.notify();
    }

    What’s wrong here?

    1. Silent Failures: The caller has no idea if the action succeeded or was ignored.
    2. Implicit undefined: Early returns give you undefined, which is a “falsy” value but not semantically meaningful.
    3. No Feedback Loop: You can’t conditionally trigger side effects based on whether the action actually happened.

    The Solution: The Try Pattern

    The Try Pattern is simple: your method “tries” to perform an action and returns true if it succeeded, false if it didn’t.

    public handleUserAction(action: UserAction): boolean {
      if (this.lifecycleState !== 'Active') return false;
      
      const nextState = this.transition(this.currentState, action);
      if (!nextState) return false;
      
      this.currentState = nextState;
      this.notify();
      return true;
    }

    Why This Matters: Three Key Benefits

    1. The Try Pattern: Clear Intent

    The function name and return type together communicate intent:

    “I will try to handle this user action. If I succeed, you’ll get true. If the conditions aren’t right or the transition is invalid, you’ll get false.”

    This is self-documenting code. A developer reading your API immediately understands that this method might not always succeed.

    2. Caller Flexibility

    Returning a boolean gives the caller options without forcing them to use it.

    Scenario A: Simple Usage (Ignore the Result)

    stateManager.handleUserAction('EXPAND');
    // Works fine. Return value is ignored.</em>
    

    Scenario B: Advanced Usage (React to Success/Failure)

    const success = stateManager.handleUserAction('EXPAND');
    
    if (!success) {
      // Play error sound</em>
      playErrorBuzz();
      
      // Show a tooltip</em>
      showTooltip('Action cannot be performed at this time');
      
      // Log analytics</em>
      analytics.track('action_failed', { action: 'submit' });
    }

    The beauty here is that you’re not forcing complexity on simple use cases, but you’re enabling advanced use cases when needed.

    3. Avoiding undefined Pitfalls

    When you don’t specify a return type and use early returns, TypeScript infers the return type as void | undefined. This can lead to subtle bugs:

    // ❌ Without explicit boolean return
    public handleUserAction(action: UserAction) {
      if (this.state !== 'Active') return; // Returns undefined
      // ... do stuff
    }
    
    // Later in code:
    if (handleUserAction('EXPAND')) {
      // This block will NEVER run, even on success!
      // Because the function returns undefined, which is falsy.
    }
    

    With an explicit boolean return type:

    // ✅ With explicit boolean return</em>
    public handleUserAction(action: UserAction): boolean {
      if (this.state !== 'Active') return false; // Explicit false</em>
      // ... do stuff</em>
      return true; // Explicit success</em>
    }
    
    // Later in code:</em>
    if (handleUserAction('EXPAND')) {
      // This works as expected!</em>
      showSuccessMessage();
    }

    When NOT to Use This Pattern

    This pattern is best for commands or actions that might fail. It’s not appropriate for:

    1. Getters: Methods that just retrieve data (getState()isActive())
    2. Pure Calculations: Functions that always succeed (calculateTotal()formatDate())
    3. Async Operations: Use Promise<boolean> or Result<T, E> types instead

    Summary: The Professional Standard

    Always return a boolean for methods that:

    • Perform state changes
    • Execute user actions
    • Might fail due to preconditions

    Benefits:

    • Self-documenting intent (the “Try Pattern”)
    • Caller flexibility (simple or advanced usage)
    • Avoids undefined pitfalls
    • Consistent, predictable API

    ☑️ Real-world impact:

    • Easier debugging (you can log failures)
    • Better UX (you can show error messages)
    • Cleaner analytics (you can track invalid actions)

    Bottom line: If you’re writing a method that might not always succeed, return a boolean. Your future self (and your teammates) will thank you.

  • Guard Clauses: Sharpen Your Logic

    Guard Clauses: Sharpen Your Logic

    The Pain: When Code Becomes a Maze

    I still remember debugging a Hero Image and caption detection function at 12AM. Several levels of nesting. I sat there, coffee cold, counting indentation levels like some deranged accountant, muttering “which branch am I even in?”

    You’ve been there too, haven’t you? You open a function, and your eyes start scanning. One if statement. Then another nested inside. Then another. The actual business logic? Buried somewhere past line 30, five indentation levels deep, like treasure at the bottom of a badly designed dungeon.

    You need to debug a null reference error. Simple enough, right? Wrong. First, you have to mentally trace through a pyramid of conditions just to understand when the code even reaches the problematic line. Ten seconds pass. You’re still mapping the control flow in your head. Your cursor is hovering somewhere in the middle, and you’ve lost track of which else belongs to which if.

    This is the reality of deeply nested conditionals. The “happy path” the main logic your function exists to perform becomes obscured by layers of defensive checks. During code reviews, you find yourself scrolling, counting braces, trying to remember which condition you’re currently inside.

    The cognitive overhead is real. And it compounds with every new edge case you need to handle.

    I wish someone had shown me guard clauses five years earlier.

    What Is a Guard Clause?

    guard clause is an early exit that prevents invalid states from flowing deeper into a function.

    It’s a conditional statement placed at the beginning of a function that checks for exceptional, invalid, or edge-case conditions. When such a condition is detected, the guard clause immediately exits the function typically via return, throw, or continue.

    Key Characteristics

    • Positioned at the top of the function
    • Handles invalid, exceptional, or edge cases (not the main logic)
    • Exits early using return, throw, or continue
    • Inverts the condition compared to traditional nested if statements

    Guard clauses are not about adding more conditions. They’re about reorganizing existing conditions to make the code’s intent clearer.

    Before vs After: Visual Proof

    The difference between nested conditionals and guard clauses is best understood visually.

    ❌ Without Guard Clauses

    function processOrder(order) {
      if (order !== null) {
        if (order.items.length > 0) {
          if (order.customer !== null) {
            if (order.customer.isVerified) {
              // Finally, the actual business logic
              const total = calculateTotal(order.items);
              const discount = applyDiscount(order.customer);
              const finalAmount = total - discount;
              
              return createInvoice(order, finalAmount);
            } else {
              throw new Error('Customer not verified');
            }
          } else {
            throw new Error('Customer is required');
          }
        } else {
          throw new Error('Order must contain items');
        }
      } else {
        throw new Error('Order cannot be null');
      }
    }

    Problems:

    • Business logic is buried at indentation level 5
    • Error messages are in reverse order (most specific first)
    • Hard to scan you must read the entire function to find the happy path
    • Adding a new validation requires navigating the nesting structure

    ✅ With Guard Clauses

    function processOrder(order) {
      // Guard clauses: handle invalid states first
      if (order === null) {
        throw new Error('Order cannot be null');
      }
      
      if (order.items.length === 0) {
        throw new Error('Order must contain items');
      }
      
      if (order.customer === null) {
        throw new Error('Customer is required');
      }
      
      if (!order.customer.isVerified) {
        throw new Error('Customer not verified');
      }
      
      // Happy path: the actual business logic
      const total = calculateTotal(order.items);
      const discount = applyDiscount(order.customer);
      const finalAmount = total - discount;
      
      return createInvoice(order, finalAmount);
    }

    Benefits:

    • Zero indentation for the main logic
    • Validations read in logical order (general to specific)
    • Happy path is immediately visible
    • Adding new validations is trivial just insert another guard clause

    The visual contrast is undeniable. The second version reads like a checklist: verify preconditions, then execute the core logic.

    Why Guard Clauses Improve Code

    Guard clauses aren’t just a stylistic preference. They provide measurable improvements to code quality.

    🔍 Readability: Top-to-Bottom Scanning

    Human eyes scan code from top to bottom. Guard clauses align with this natural reading pattern. You see the preconditions first, then the main logic. No mental stack required to track which branch you’re in.

    🧠 Cognitive Load: Fewer Branches to Track

    Nested conditionals force you to maintain a mental model of “where you are” in the branching tree. Guard clauses eliminate this. Each check is independent. If you pass all guards, you’re in the happy path. Simple.

    🧪 Debugging: Fail Fast, Fail Clearly

    When something goes wrong, guard clauses make it obvious where the failure occurred. The function exits immediately at the point of failure. No need to trace through nested blocks to find which condition wasn’t met.

    🔧 Maintainability: Easy to Add New Checks

    Need to add a new validation? With guard clauses, you simply add another check at the top. With nested conditionals, you need to find the right place in the nesting structure and potentially refactor existing code.

    📊 Testability: Isolated Conditions

    Each guard clause represents a distinct test case. Testing becomes straightforward: provide input that triggers each guard, and verify the expected early exit. With nested conditionals, you often need complex setup to reach deeply nested branches.

    When Guard Clauses Are Appropriate

    Guard clauses are powerful, but they’re not a universal solution. Understanding when to use them is key to writing mature, maintainable code.

    ✅ Good Use Cases

    Input Validation

    function createUser(username, email, password) {
      if (!username || username.trim() === '') {
        throw new Error('Username is required');
      }
      
      if (!email || !isValidEmail(email)) {
        throw new Error('Valid email is required');
      }
      
      if (!password || password.length < 8) {
        throw new Error('Password must be at least 8 characters');
      }
      
      // Proceed with user creation
    }

    Permission Checks

    function deletePost(post, user) {
      if (!user.isAuthenticated) {
        throw new UnauthorizedError('User must be logged in');
      }
      
      if (post.authorId !== user.id && !user.isAdmin) {
        throw new ForbiddenError('Insufficient permissions');
      }
      
      // Proceed with deletion
    }

    Null/Undefined/Empty States

    function getFirstItem(array) {
      if (!array || array.length === 0) {
        return null;
      }
      
      return array[0];
    }

    Preconditions

    function calculateDiscount(order) {
      if (order.total < 100) {
        return 0; // No discount for orders under $100
      }
      
      if (!order.customer.isPremium) {
        return order.total * 0.05; // 5% for regular customers
      }
      
      // Premium customer logic
      return order.total * 0.15;
    }

    ⚠️ Questionable Use Cases

    Extremely Trivial One-Liners

    If your entire function is just a simple check and return, a guard clause might be overkill:

    // Overkill
    function isPositive(num) {
      if (num <= 0) return false;
      return true;
    }
    
    // Better
    function isPositive(num) {
      return num > 0;
    }

    Complex Branching Logic That Needs Structure

    When you have multiple distinct behaviors (not just validation vs. main logic), structured branching might be clearer:

    // Guard clauses don't help here
    function processPayment(payment) {
      if (payment.method === 'credit_card') {
        return processCreditCard(payment);
      }
      
      if (payment.method === 'paypal') {
        return processPayPal(payment);
      }
      
      if (payment.method === 'crypto') {
        return processCrypto(payment);
      }
      
      throw new Error('Unknown payment method');
    }

    When Early Exits Obscure Lifecycle Flow

    In functions with setup/teardown or resource management, early exits can make cleanup logic harder to track:

    // Problematic
    function processFile(filePath) {
      const file = openFile(filePath);
      
      if (!file.isReadable()) {
        return; // Oops, file not closed
      }
      
      const data = file.read();
      file.close();
      return data;
    }
    
    // Better: use try-finally or structured approach
    function processFile(filePath) {
      const file = openFile(filePath);
      
      try {
        if (!file.isReadable()) {
          throw new Error('File not readable');
        }
        
        return file.read();
      } finally {
        file.close();
      }
    }

    Guard Clauses vs Nested Conditionals: A Decision Guide

    The choice between guard clauses and nested conditionals comes down to one key distinction:

    🚫 If the condition invalidates execution → Guard Clause

    When a condition means “we cannot proceed,” use a guard clause:

    function withdraw(account, amount) {
      if (amount <= 0) {
        throw new Error('Amount must be positive');
      }
      
      if (account.balance < amount) {
        throw new Error('Insufficient funds');
      }
      
      // Proceed with withdrawal
      account.balance -= amount;
    }

    🔀 If the condition changes behavior → Structured Branching

    When a condition determines how to proceed (not whether to proceed), use structured conditionals:

    function calculateShipping(order) {
      if (order.total > 100) {
        return 0; // Free shipping
      } else if (order.isExpress) {
        return 20; // Express shipping
      } else {
        return 10; // Standard shipping
      }
    }

    Mixed Scenarios

    Often, you’ll use both in the same function: guards for validation, then structured logic for behavior:

    function applyPromoCode(order, promoCode) {
      // Guards: validate preconditions
      if (!order || order.items.length === 0) {
        throw new Error('Invalid order');
      }
      
      if (!promoCode || promoCode.trim() === '') {
        return order; // No promo code, return unchanged
      }
      
      // Structured logic: different behaviors based on code type
      if (promoCode.startsWith('PERCENT')) {
        order.discount = order.total * 0.1;
      } else if (promoCode.startsWith('FIXED')) {
        order.discount = 10;
      } else {
        throw new Error('Invalid promo code');
      }
      
      return order;
    }

    Common Mistakes ⚠️

    Even experienced developers can misuse guard clauses. Here are the pitfalls to avoid:

    Too Many Guards with No Clear Priority

    // Confusing: which check is most important?
    function processRequest(req) {
      if (!req.user) throw new Error('No user');
      if (!req.data) throw new Error('No data');
      if (!req.timestamp) throw new Error('No timestamp');
      if (!req.signature) throw new Error('No signature');
      if (!req.apiKey) throw new Error('No API key');
      if (!req.version) throw new Error('No version');
      
      // ... more guards ...
    }

    Fix: Group related guards and add comments to show logical progression:

    function processRequest(req) {
      // Authentication
      if (!req.apiKey) throw new Error('No API key');
      if (!req.user) throw new Error('No user');
      
      // Request validation
      if (!req.data) throw new Error('No data');
      if (!req.timestamp) throw new Error('No timestamp');
      
      // Security
      if (!req.signature) throw new Error('No signature');
      
      // Proceed...
    }

    Guards That Perform Business Logic

    // Wrong: guard clause doing too much
    function updateProfile(user, newData) {
      if (!user.isPremium && !upgradeToPremium(user)) {
        throw new Error('Premium required');
      }
      
      // Update profile...
    }

    Fix: Guards should only check conditions, not change state:

    function updateProfile(user, newData) {
      if (!user.isPremium) {
        throw new Error('Premium required');
      }
      
      // Update profile...
    }

    Throwing Exceptions Where a Return Is Enough

    // Overly dramatic
    function findUser(id) {
      if (!id) {
        throw new Error('ID required'); // Is this really exceptional?
      }
      
      return database.findById(id);
    }
    

    Fix: Use exceptions for truly exceptional cases:

    function findUser(id) {
      if (!id) {
        return null; // Missing ID is a normal case</em>
      }
      
      return database.findById(id);
    }

    Guard Clauses Scattered Mid-Function

    // Confusing: guards mixed with logic
    function processOrder(order) {
      const items = order.items;
      
      if (!order.customer) {
        throw new Error('No customer');
      }
      
      const total = calculateTotal(items);
      
      if (items.length === 0) {
        throw new Error('No items');
      }
      
      return total;
    }

    Fix: All guards at the top, logic below:

    function processOrder(order) {
      // All guards first
      if (!order.customer) {
        throw new Error('No customer');
      }
      
      if (order.items.length === 0) {
        throw new Error('No items');
      }
      
      // Then logic
      const total = calculateTotal(order.items);
      return total;
    }

    Negating Conditions Unnecessarily

    // Harder to read
    function isEligible(user) {
      if (!(user.age >= 18)) {
        return false;
      }
      
      if (!(user.isVerified === true)) {
        return false;
      }
      
      return true;
    }

    Fix: Use positive, natural conditions:

    function isEligible(user) {
      if (user.age < 18) {
        return false;
      }
      
      if (!user.isVerified) {
        return false;
      }
      
      return true;
    }

    Conclusion: Clarity Through Structure

    Guard clauses are not about writing “cleaner” code they’re about writing code that aligns with how developers think and read.

    By handling exceptional cases first and keeping the happy path visible, guard clauses reduce cognitive load, improve debuggability, and make maintenance straightforward.

    They’re not appropriate everywhere. But when you’re validating inputs, checking permissions, or handling edge cases, guard clauses transform tangled nesting into clear, linear logic.

    The next time you write a function, ask yourself: “What conditions invalidate this operation?” Handle those first. Then write the logic that matters.

    Your future self and your teammates will thank you.