Guard Clauses & Early Return — Practice Tasks¶
Category: Control-Flow Patterns — handle invalid and edge cases up front, then return, keeping the happy path un-nested.
10 graded hands-on tasks with full Go, Java, and Python solutions. Try each before expanding the solution.
Table of Contents¶
- Task 1: Flatten the Arrow
- Task 2: Invert the Condition
- Task 3: Drop the Dangling else
- Task 4: Validate Before Mutating
- Task 5: Guard + Cleanup with Scope
- Task 6: In-Loop Guards with continue
- Task 7: Throw vs Return-Default
- Task 8: Order the Guards
- Task 9: Push the Guard into a Type
- Task 10: Split an Over-Guarded Function
- Practice Tips
Task 1: Flatten the Arrow¶
Goal: Rewrite a 4-level nested function as a flat sequence of guard clauses. Behavior must be identical.
Given (Python):
def grade(score):
if score is not None:
if 0 <= score <= 100:
if score >= 60:
return "pass"
else:
return "fail"
else:
raise ValueError("score out of range")
else:
raise ValueError("score required")
Solution
### Pythondef grade(score):
if score is None: raise ValueError("score required")
if not (0 <= score <= 100): raise ValueError("score out of range")
if score < 60: return "fail"
return "pass"
Task 2: Invert the Condition¶
Goal: Take a function whose work is wrapped in if (good) { ... } and invert it so the bad case leaves first.
Given (Java):
Solution
### Java ### Python ### Go **Why:** Inversion un-nests the body. The work now lives at column zero, not inside an `if`.Task 3: Drop the Dangling else¶
Goal: Remove every else that follows a returning if.
Given (Go):
func classify(n int) string {
if n < 0 {
return "negative"
} else if n == 0 {
return "zero"
} else {
return "positive"
}
}
Solution
### Gofunc classify(n int) string {
if n < 0 { return "negative" }
if n == 0 { return "zero" }
return "positive"
}
Task 4: Validate Before Mutating¶
Goal: A function mutates state and then validates — fix the ordering so no guard can fire after a side effect.
Given (Python) — buggy:
def apply_discount(cart, code):
cart.total *= 0.9 # mutates first!
if code not in VALID_CODES:
raise ValueError("invalid code") # too late — total already changed
cart.applied_code = code
Solution
### Pythondef apply_discount(cart, code):
if code not in VALID_CODES: # guard BEFORE mutation
raise ValueError("invalid code")
cart.total *= 0.9
cart.applied_code = code
Task 5: Guard + Cleanup with Scope¶
Goal: Add an early-return guard to a function that holds a resource, without leaking it.
Given (Go) — buggy: early return leaks the lock:
func (c *Cache) GetOrLoad(key string) (Value, error) {
c.mu.Lock()
if v, ok := c.data[key]; ok {
return v, nil // returns while holding the lock!
}
v, err := load(key)
if err != nil {
return Value{}, err // also leaks the lock
}
c.data[key] = v
c.mu.Unlock()
return v, nil
}
Solution
### Gofunc (c *Cache) GetOrLoad(key string) (Value, error) {
c.mu.Lock()
defer c.mu.Unlock() // released on EVERY return
if v, ok := c.data[key]; ok {
return v, nil
}
v, err := load(key)
if err != nil {
return Value{}, err
}
c.data[key] = v
return v, nil
}
Task 6: In-Loop Guards with continue¶
Goal: Flatten a loop body using continue as the loop's early return.
Given (Java):
Solution
### Java ### Python ### Go **Why:** `continue` skips the iteration the same way `return` skips the function. The loop body stays flat.Task 7: Throw vs Return-Default¶
Goal: Decide, per case, whether a guard should throw (caller error) or return a default (expected edge). Implement both correctly.
Spec: discount(user) — a missing user is an expected edge (return 0). A user with a null tier is a data-integrity bug (throw).
Solution
### Pythondef discount(user):
if user is None:
return 0.0 # expected edge → default
if user.tier is None:
raise ValueError("user has no tier") # invariant broken → throw
return user.tier.rate
Task 8: Order the Guards¶
Goal: Reorder guards so each may safely assume the ones above passed (existence → shape → value). The given code dereferences before checking for null.
Given (Python) — buggy order:
def first_item_name(order):
if order.items[0].name == "": # dereferences before null/empty checks!
raise ValueError("blank name")
if order is None:
raise ValueError("no order")
if not order.items:
raise ValueError("empty order")
return order.items[0].name
Solution
### Pythondef first_item_name(order):
if order is None: raise ValueError("no order") # existence
if not order.items: raise ValueError("empty order") # shape
if order.items[0].name == "":raise ValueError("blank name") # value
return order.items[0].name
String firstItemName(Order order) {
if (order == null) throw new IllegalArgumentException("no order");
if (order.items().isEmpty()) throw new IllegalArgumentException("empty order");
if (order.items().get(0).name().isBlank())
throw new IllegalArgumentException("blank name");
return order.items().get(0).name();
}
Task 9: Push the Guard into a Type¶
Goal: A raw value is guarded in many functions. Parse it once into a type so downstream code needs no guard ("parse, don't validate").
Given (Java) — every function re-checks the percentage:
void setOpacity(double pct) {
if (pct < 0 || pct > 100) throw new IllegalArgumentException();
// ...
}
void setVolume(double pct) {
if (pct < 0 || pct > 100) throw new IllegalArgumentException(); // duplicated
// ...
}
Solution
### Javarecord Percentage(double value) {
Percentage { // guard fires ONCE, at construction
if (value < 0 || value > 100)
throw new IllegalArgumentException("percentage 0..100, got " + value);
}
}
void setOpacity(Percentage pct) { /* no guard — type guarantees validity */ }
void setVolume(Percentage pct) { /* no guard */ }
from dataclasses import dataclass
@dataclass(frozen=True)
class Percentage:
value: float
def __post_init__(self):
if not (0 <= self.value <= 100):
raise ValueError(f"percentage 0..100, got {self.value}")
def set_opacity(pct: Percentage): ... # no guard
def set_volume(pct: Percentage): ... # no guard
type Percentage struct{ value float64 }
func NewPercentage(v float64) (Percentage, error) {
if v < 0 || v > 100 {
return Percentage{}, fmt.Errorf("percentage 0..100, got %v", v)
}
return Percentage{v}, nil
}
func setOpacity(p Percentage) { /* no guard */ }
func setVolume(p Percentage) { /* no guard */ }
Task 10: Split an Over-Guarded Function¶
Goal: A function with 7 guards spanning multiple subsystems is doing too much. Split it so each function trusts validated inputs.
Given (Python) — too many responsibilities:
def submit(order, user, inventory, payment):
if order is None: raise ValueError("order")
if not order.items: raise ValueError("items")
if user is None: raise ValueError("user")
if not user.verified: raise PermissionError("unverified")
if not inventory.has(order.items): raise ValueError("out of stock")
if payment is None: raise ValueError("payment")
if payment.declined: raise PaymentError("declined")
return place(order, user, payment)
Solution
### Pythondef validate_order(order):
if order is None: raise ValueError("order")
if not order.items: raise ValueError("items")
def validate_user(user):
if user is None: raise ValueError("user")
if not user.verified: raise PermissionError("unverified")
def validate_payment(payment):
if payment is None: raise ValueError("payment")
if payment.declined: raise PaymentError("declined")
def submit(order, user, inventory, payment):
validate_order(order)
validate_user(user)
validate_payment(payment)
if not inventory.has(order.items):
raise ValueError("out of stock")
return place(order, user, payment) # orchestration only
Practice Tips¶
- Always end a guard with an exit —
return/throw/continue/break. A logging-only guard is a bug. - Guard before you mutate. Never leave partial state behind a failed check.
- Order: existence → shape → value. Each guard assumes the ones above passed.
- Bind cleanup to scope (
defer/finally/with) so early returns can't leak. - Throw for caller errors; return a default only for expected edges.
- When guards pile up, split the function and consider pushing validation into a type.
← Interview · Control-Flow · Roadmap · Next: Find-Bug
In this topic