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.