Early return pattern — guard clauses to reduce nesting
Concept
The early return pattern (guard clauses) inverts nested conditionals into flat sequential checks. Instead of wrapping the happy path in a deeply nested if chain, you handle each failure condition at the top of the function and return immediately. The happy path is the unindented code at the bottom.
The psychological effect matters: a reader sees the exceptional cases and returns first (fast understanding of what breaks), then reads the main logic uninterrupted. Compare this to a function where the main logic is buried inside three levels of if.
Rules of thumb:
- If a function has more than 2 levels of nesting from conditionals, consider guard clauses.
- Input validation, authorization, null checks, and precondition checks are almost always better as early returns.
- A function should have one primary "success" return at the end; early returns are for failures and edge cases.
Exceptions to the pattern: Short functions with simple branching don't need guard clauses — the overhead of converting isn't worth it. Also, if the code is a complex decision tree (e.g., a pricing calculator), guard clauses may not improve clarity over a well-structured set of conditions.
This pattern is part of a broader "fail fast" philosophy: detect problems as early and as specifically as possible, instead of accumulating state and failing later with less context.
Code Example
<?php
declare(strict_types=1);
// BEFORE: deeply nested (hard to read)
function createUser_nested(array $data): array|false
{
if (!empty($data['email'])) {
if (filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
if (!empty($data['password'])) {
if (strlen($data['password']) >= 8) {
if (!userExists($data['email'])) {
return saveUser($data);
}
}
}
}
}
return false;
}
// AFTER: guard clauses — flat, fast-fail
function createUser(array $data): array
{
if (empty($data['email'])) {
throw new \InvalidArgumentException('Email is required');
}
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Email is invalid');
}
if (empty($data['password'])) {
throw new \InvalidArgumentException('Password is required');
}
if (strlen($data['password']) < 8) {
throw new \InvalidArgumentException('Password too short');
}
if (userExists($data['email'])) {
throw new \RuntimeException('Email already registered');
}
return saveUser($data); // happy path — no nesting
}
// Guard clauses in authorization checks (Laravel controller style)
public function update(Request $request, Post $post): Response
{
if (!$request->user()->can('update', $post)) {
abort(403);
}
if ($post->isPublished()) {
abort(422, 'Cannot edit a published post');
}
$post->update($request->validated());
return response()->noContent();
}
// Guard clauses with null checks
function formatUserAddress(?User $user): string
{
if ($user === null) return 'Unknown';
if ($user->address === null) return 'No address on file';
if ($user->address->country === null) return $user->address->city;
return "{$user->address->city}, {$user->address->country}";
}