Guard Clauses
Flatten the pyramid of doom.
The arrow anti-pattern buries the happy path under unrelated checks. Lift each abnormal case to a guarded early return. The remaining body reads top-to-bottom as the success story.
Opinion
The arrow anti-pattern is the default thing engineers write before they have read Refactoring.1 A function takes a value, checks it is not null, opens a brace. It checks it is the right type, opens another. It checks the user has permission, opens another. The work then sits at four levels of indentation in the middle of the function with the closing braces stacked beneath it like a pyramid in reverse. The reader has to hold every condition in working memory while looking for the body. The body is not the point of the function. The preconditions are.
The fix is operational. Lift each precondition to a guarded early return. The function reads top-to-bottom as a sequence of failure cases followed by the happy path. The happy path is indented once. The closing braces are gone. Linus put it in three sentences in the kernel coding style: if you need more than 3 levels of indentation, you're screwed anyway, and should fix your program.2 The kernel has compiled cleanly under that rule for thirty years. It works.
The pairing matters. Guard clauses depend on S2 Cyclomatic Caps firing at depth 3: without the cap, the arrow anti-pattern survives because nobody is forced to deal with it. They depend on F1 Single Responsibility deciding whether the abnormal case is a guard or a separate function: sometimes the right answer is to extract the precondition into its own validator. The shape that falls out is why the function is easy to read. The shape is the point.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
Lift each precondition to a guarded early return at the top of the function. The body that survives is the happy path, indented once. The arrow anti-pattern (deep nested conditionals) buries the meaningful work. Flatten it. /tenet/guard-clauses/S4
AI eyes only
Rule: flatten the pyramid of doom. Early returns over nested ifs.
Reject: nested if-else chains beyond two levels. Reject: indent depth above three inside one function. Reject: else after a return.
Generate: validate, then return. Precondition checks at the top of the function. The happy path stays at the lowest indentation level.
Diagnostic: count nesting depth. Four or more inside one function: restructure into guard clauses.
Why?
- The function's happy path reads top-to-bottom as a paragraph at one indentation level. The next reviewer can hold the body in their head without scrolling.
- Each precondition becomes a named guard with its own failure mode. The function's contract is visible at the top, not buried in the middle.
- Nesting depth drops by one for every nested conditional that becomes a guard. Pairs with S2 Cyclomatic Caps: the cap fires. The guard fixes the cap.
- Each guard clause is one branch with one failure mode. Coverage rises almost for free. The tests target a smaller surface and the failure modes are obvious from the function's shape.
- Coding agents stop generating nested-conditional pyramids when the lint cap on depth fires. The shape that the cap rewards is the shape the canon prescribes.
- Fail-fast debugging falls out of the shape. When something goes wrong, the failure happens at the top of the function, near the input that caused it. The stack trace points at the guard, not at the work site five levels deep.
- Cuts review time. Reviewers stop arguing about whether a function is too nested. The lint decides. The conversation moves to whether the guards are well-named.
Origins
Martin Fowler catalogued Replace Nested Conditional with Guard Clauses in Refactoring in 1999 and reprinted it in the second edition in 2018.1 The catalogue entry names the move precisely: when a function's conditionals make it hard to see the normal path of execution, lift each abnormal case to a separate check that returns early. The function's preconditions are made explicit. The body becomes the happy path. The indentation drops by one level for every check that becomes a guard.6
The rule the move enforces is older than the catalogue. Linus Torvalds put it in three sentences in the Linux kernel coding style:2 “If you need more than 3 levels of indentation, you're screwed anyway, and should fix your program.” The kernel has shipped under that rule for thirty years. The rule survives because it is right. The kernel community uses goto cleanup; labels as the assembly-friendly form of the same instinct: exit early on failure, fall through on success.
refactoring.guru holds the canonical worked example with code samples in multiple languages.4 The before-and-after pattern is unambiguous: the nested-conditional version is always longer, always less readable, and always harder to test. The guard-clause version front-loads the failure cases. The happy path becomes the function's thesis statement.
Kent Beck's Smalltalk Best Practice Patterns (1996) gave the larger underlying pattern.5 Composed Method: every method is composed of other methods at a single level of abstraction. Guard clauses are the operational form of the rule at the function level: the preconditions are at one level of abstraction, the body is at another and the early return separates them physically.
Quotes
I often find that conditional expressions come in two styles. In the first style, both legs of the conditional are part of normal behavior, while in the second style, one leg is normal and the other indicates an unusual condition. … If the condition is an unusual condition, I check the condition and return if it's true. This kind of check is often called a guard clause.
The answer to that is that if you need more than 3 levels of indentation, you're screwed anyway, and should fix your program.
Divide your program into methods that perform one identifiable task. Keep all of the operations in a method at the same level of abstraction.
You have a group of nested conditionals and it’s hard to determine the normal flow of code execution. … Isolate all special checks and edge cases into separate clauses and place them before the main checks.
Evidence
16 external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01The canonical refactor. Lift each abnormal case to an early return. The body becomes the happy path. Indentation drops by one level for every conversion.
- 02The teaching form of Fowler's catalogue entry. Worked examples in Java, C#, Python, TypeScript. The before-and-after pattern is unambiguous.
- 03Linux kernel coding style §1SupportsThree indentation levels max. The kernel's checkpatch.pl warns only at six or more leading tabs (DEEP_INDENTATION). Thirty years of working under the rule across a million-line C codebase.
- 04The larger pattern. Single level of abstraction per method. The guard-clause shape falls out as the operational form of the rule.
- 05Clean Code, ch. 3 “Functions”SupportsSame instinct, different vocabulary. Small functions, one indentation level, abnormal cases lifted to guards. The pattern survived the editorial controversy because the shape is right.
- 06“Go To Statement Considered Harmful”QualifiesThe historical antagonist of multiple-exit functions. Read in context, the argument is against unbridled go to in higher-level languages (everything except, perhaps, plain machine code), not against structured early returns. The modern reading is more nuanced.
- 07Safety-critical embedded code that must satisfy formal-verification tools sometimes requires single-exit functions. Bounded counter-position. Application code has a different cost model.
- 08ESLint: `max-depth` ruleSupportsThe canonical web surface. Default 4. The Prickles repo sets it at 3 to match Linus. The cap forces the conversation that produces the guard clause.
- 09Crash early. Fail fast. Do not pass bad data forward. The instinct underneath the guard clause: detect invalid input at the input, not at the work site.
- 10ESLint: `no-else-return` ruleSupportsCatches the if-return-else-something pattern that guard clauses retire. The companion lever to `max-depth` for cleaning up nested conditionals after the cap fires.
- 11Python implementation of the depth cap. Default 5. Tighten to 3 to match the editorial position; Ruff inherits the rule under the same identifier.
- 12Modern Python surface for the same cap. Single binary, single TOML block. The right choice for new Python projects.
- 13PMD: AvoidDeeplyNestedIfStmtsSupportsJava implementation of the depth cap. Default 3. The canonical Java lint surface for the rule.
- 14PHP implementation of the depth cap. Default 5. Tighten to 3 for parity with the JavaScript and Python surfaces.
- 15“Nothing is Something”QualifiesTalk on the Null Object pattern. The larger instinct: treat the abnormal case as a real value with its own behaviour, not an exception to the happy path. Sometimes the right answer is no guard at all.
- 16Restatement of Beck's Composed Method pattern in modern refactoring terms. Pairs the pattern with Extract Method as the operational steps to flatten a nested function.
Sixteen sources, three stances. The supporters cluster on the move itself: Fowler, Beck, refactoring.guru, Linus. The qualifiers hold the “multiple returns are harder to reason about” argument, Dijkstra at its head and read back in context. MISRA C:2012 Rule 15.5 opposes it. That advisory rule asks for a single point of exit; MISRA C:2025 has since disapplied it. The disagreement is about return-style discipline, not about flattening.
Examples
Before one function checking four concerns. Pyramid of doom shape.
function canFeedHedgehog(hedgehog: Hedgehog | null, food: Food): boolean { if (hedgehog) { if (!hedgehog.isHibernating) { if (hedgehog.weightG > 600) { if (food.isSafe) { return true; } } } } return false; }
After each concern is its own predicate. The orchestrator has one guard, then combines.
function isHedgehogReady(hedgehog: Hedgehog): boolean { return !hedgehog.isHibernating && hedgehog.weightG > 600; } function isFoodSuitable(food: Food): boolean { return food.isSafe; } const canFeedHedgehog = (hedgehog: Hedgehog | null, food: Food): boolean => hedgehog ? isHedgehogReady(hedgehog) && isFoodSuitable(food) : false;
Before one function checking four concerns. Pyramid of doom shape.
function canFeedHedgehog(hedgehog: Hedgehog | null, food: Food): boolean { if (hedgehog) { if (!hedgehog.isHibernating) { if (hedgehog.weightG > 600) { if (food.isSafe) { return true; } } } } return false; }
After each concern is its own predicate. The orchestrator has one guard, then combines.
function isHedgehogReady(hedgehog: Hedgehog): boolean { return !hedgehog.isHibernating && hedgehog.weightG > 600; } function isFoodSuitable(food: Food): boolean { return food.isSafe; } const canFeedHedgehog = (hedgehog: Hedgehog | null, food: Food): boolean => hedgehog ? isHedgehogReady(hedgehog) && isFoodSuitable(food) : false;
Before one function checking four concerns. Pyramid of doom shape.
function canFeedHedgehog(hedgehog, food) { if (hedgehog) { if (!hedgehog.isHibernating) { if (hedgehog.weightG > 600) { if (food.isSafe) { return true; } } } } return false; }
After each concern is its own predicate. The orchestrator has one guard, then combines.
function isHedgehogReady(hedgehog) { return !hedgehog.isHibernating && hedgehog.weightG > 600; } function isFoodSuitable(food) { return food.isSafe; } const canFeedHedgehog = (hedgehog, food) => hedgehog ? isHedgehogReady(hedgehog) && isFoodSuitable(food) : false;
Before one function checking four concerns. Pyramid of doom shape.
def can_feed_hedgehog(hedgehog: Hedgehog | None, food: Food) -> bool: if hedgehog is not None: if not hedgehog.is_hibernating: if hedgehog.weight_g > 600: if food.is_safe: return True return False
After each concern is its own predicate. The orchestrator has one guard, then combines.
def is_hedgehog_ready(hedgehog: Hedgehog) -> bool: return not hedgehog.is_hibernating and hedgehog.weight_g > 600 def is_food_suitable(food: Food) -> bool: return food.is_safe def can_feed_hedgehog(hedgehog: Hedgehog | None, food: Food) -> bool: return is_hedgehog_ready(hedgehog) and is_food_suitable(food) if hedgehog else False
Before one method checking four concerns. Pyramid of doom shape.
static boolean canFeedHedgehog(Hedgehog hedgehog, Food food) { if (hedgehog != null) { if (!hedgehog.isHibernating()) { if (hedgehog.weightG() > 600) { if (food.isSafe()) { return true; } } } } return false; }
After each concern is its own predicate. The orchestrator has one guard, then combines.
static boolean isHedgehogReady(Hedgehog hedgehog) { return !hedgehog.isHibernating() && hedgehog.weightG() > 600; } static boolean isFoodSuitable(Food food) { return food.isSafe(); } static boolean canFeedHedgehog(Hedgehog hedgehog, Food food) { return hedgehog != null && isHedgehogReady(hedgehog) && isFoodSuitable(food); }
Before one function checking four concerns. Pyramid of doom shape.
function canFeedHedgehog(?Hedgehog $hedgehog, Food $food): bool { if ($hedgehog !== null) { if (!$hedgehog->isHibernating) { if ($hedgehog->weightG > 600) { if ($food->isSafe) { return true; } } } } return false; }
After each concern is its own predicate. The orchestrator has one guard, then combines.
function isHedgehogReady(Hedgehog $hedgehog): bool { return !$hedgehog->isHibernating && $hedgehog->weightG > 600; } function isFoodSuitable(Food $food): bool { return $food->isSafe; } function canFeedHedgehog(?Hedgehog $hedgehog, Food $food): bool { return $hedgehog ? isHedgehogReady($hedgehog) && isFoodSuitable($food) : false; }
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| max-depth | ESLint core | indentation depth above the cap. The lever Linus names directly. Three is the kernel's number; the Prickles repo agrees. |
| no-else-return | ESLint core | the if-return-else-something pattern that guard clauses retire. With allowElseIf: false, the rule rewrites if-else-if chains into early returns. |
| no-lonely-if | ESLint core | the lonely if-inside-else that should have been an else-if. The micro-cousin of the arrow anti-pattern. |
| unicorn/no-negated-condition | eslint-plugin-unicorn | negated conditions that read if (!X) { … } else { … }: usually the guard you want to lift, expressed inside-out. |
| unicorn/no-nested-ternary | eslint-plugin-unicorn | nested ternary expressions: the inline form of the arrow anti-pattern. Drives the rewrite into named guards. |
| sonarjs/no-collapsible-if | eslint-plugin-sonarjs | an if nested alone inside another if, which could be combined with &&. One guard for two conditions; the rule reports it and offers no autofix. |
eslint.config.mjsconfiguration snippet
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import unicorn from 'eslint-plugin-unicorn';
import sonarjs from 'eslint-plugin-sonarjs';
export default defineConfig({
files: ['**/*.{ts,tsx}'],
languageOptions: { parser: tseslint.parser },
plugins: { unicorn, sonarjs },
rules: {
'max-depth': ['error', 3],
'no-else-return': ['error', { allowElseIf: false }],
'no-lonely-if': 'error',
'unicorn/no-lonely-if': 'error',
'unicorn/no-negated-condition': 'error',
'unicorn/no-nested-ternary': 'error',
'sonarjs/no-collapsible-if': 'error',
}
});Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| max-depth | ESLint core | indentation depth above the cap. The lever Linus names directly. Three is the kernel's number; the Prickles repo agrees. |
| no-else-return | ESLint core | the if-return-else-something pattern that guard clauses retire. With allowElseIf: false, the rule rewrites if-else-if chains into early returns. |
| no-lonely-if | ESLint core | the lonely if-inside-else that should have been an else-if. The micro-cousin of the arrow anti-pattern. |
| unicorn/no-negated-condition | eslint-plugin-unicorn | negated conditions that read if (!X) { … } else { … }: usually the guard you want to lift, expressed inside-out. |
| unicorn/no-nested-ternary | eslint-plugin-unicorn | nested ternary expressions: the inline form of the arrow anti-pattern. Drives the rewrite into named guards. |
| sonarjs/no-collapsible-if | eslint-plugin-sonarjs | an if nested alone inside another if, which could be combined with &&. One guard for two conditions; the rule reports it and offers no autofix. |
eslint.config.mjsconfiguration snippet
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import unicorn from 'eslint-plugin-unicorn';
import sonarjs from 'eslint-plugin-sonarjs';
export default defineConfig({
files: ['**/*.{ts,tsx}'],
languageOptions: { parser: tseslint.parser },
plugins: { unicorn, sonarjs },
rules: {
'max-depth': ['error', 3],
'no-else-return': ['error', { allowElseIf: false }],
'no-lonely-if': 'error',
'unicorn/no-lonely-if': 'error',
'unicorn/no-negated-condition': 'error',
'unicorn/no-nested-ternary': 'error',
'sonarjs/no-collapsible-if': 'error',
}
});Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| max-depth | ESLint core | indentation depth above the cap. |
| no-else-return | ESLint core | if-return-else-something pattern. |
| no-lonely-if | ESLint core | lonely if-inside-else. |
| unicorn/no-negated-condition | eslint-plugin-unicorn | inside-out guards. |
eslint.config.mjsconfiguration snippet
import js from '@eslint/js';
import unicorn from 'eslint-plugin-unicorn';
export default [
js.configs.recommended,
{
files: ['**/*.{js,mjs,cjs}'],
plugins: { unicorn },
rules: {
'max-depth': ['error', 3],
'no-else-return': ['error', { allowElseIf: false }],
'no-lonely-if': 'error',
'unicorn/no-negated-condition': 'error',
},
},
];Apply these rules in pyproject.toml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| PLR1702 (too-many-nested-blocks) | Ruff (pylint) | indentation depth above the cap. Default 5; tighten to 3 to match the editorial position. |
| SIM102 (collapsible-if) | Ruff (flake8-simplify) | nested ifs that can be combined with and. One guard for two conditions; auto-fixable. |
| SIM103 (needless-bool) | Ruff (flake8-simplify) | if-true-else-false patterns that should return the boolean expression directly. The shape that hides behind nested conditionals. |
| SIM108 (if-else-block-instead-of-if-exp) | Ruff (flake8-simplify) | if-else blocks that should be ternary expressions. Trivial case of the arrow anti-pattern; auto-fixable. |
| RET505 / RET506 / RET507 (else-after-return) | Ruff (flake8-return) | unnecessary else / elif blocks after a return / raise / continue. The Python equivalent of no-else-return. |
| RET504 (unnecessary-assign) | Ruff (flake8-return) | value assigned to a name and then immediately returned. The companion clean-up after guards have flattened the function. |
pyproject.tomlconfiguration snippet
[tool.ruff.lint]
preview = true
select = [
"PLR1702",
"SIM102",
"SIM103",
"SIM108",
"RET504",
"RET505",
"RET506",
"RET507",
]
[tool.ruff.lint.pylint]
max-nested-blocks = 3Apply these rules in pmd-ruleset.xml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| AvoidDeeplyNestedIfStmts | PMD | if statements nested to the configured depth or deeper. PMD reports at the depth it is given, so the snippet sets 4 and three levels pass, as the tenet allows. |
| CollapsibleIfStatements | PMD | nested ifs that can be combined with &&. The trivial case of the arrow anti-pattern; PMD reports it but does not rewrite it. |
| SimplifiedTernary | PMD | ternary expressions that can be simplified. The inline shape of the arrow anti-pattern. |
| UnnecessaryReturn | PMD | redundant return statements that survive the guard-clause rewrite. The clean-up after the flattening. |
| NestedIfDepth (Checkstyle) | Checkstyle | nested if statement depth as a Checkstyle rule. Useful when Checkstyle is the team's lint surface. |
pmd-ruleset.xmlconfiguration snippet
<?xml version="1.0"?>
<ruleset name="GuardClauses"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0">
<description>S4 Guard Clauses: flatten nested conditionals.</description>
<rule ref="category/java/design.xml/AvoidDeeplyNestedIfStmts">
<properties>
<property name="problemDepth" value="4"/>
</properties>
</rule>
<rule ref="category/java/design.xml/CollapsibleIfStatements"/>
<rule ref="category/java/design.xml/SimplifiedTernary"/>
<rule ref="category/java/codestyle.xml/UnnecessaryReturn"/>
</ruleset>Apply these rules in phpmd-ruleset.xml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| ElseExpression | PHPMD | every else branch, whether or not the if leg returns. Stricter than no-else-return; flattens to a guard. |
| Generic.Metrics.NestingLevel | PHP_CodeSniffer | nesting depth per function. Set nestingLevel to 3; the sniff warns above it and errors above absoluteNestingLevel. |
| Generic.Files.LineLength | PHP_CodeSniffer | long lines that often hide nested conditionals as ternary chains. Indirect signal of the arrow anti-pattern. |
phpmd-ruleset.xmlconfiguration snippet
<?xml version="1.0"?>
<ruleset name="GuardClauses"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0">
<description>S4 Guard Clauses: flatten nested conditionals in PHP.</description>
<rule ref="rulesets/cleancode.xml/ElseExpression"/>
</ruleset>AI rules
.cursor/rules/s4-guard-clauses.mdc--- alwaysApply: true --- ## Prickles S4: Guard Clauses When a function's preconditions are nested as conditionals, lift each one to a guarded early return at the top of the function. Cap indentation depth at three. The cap is there to trigger the rewrite. When the guards themselves get long, extract them into a named validator that returns success-or-error. The function then has one guard call and the happy path body. Refuse to add a fourth indentation level to a function. Either the function is doing too much and needs to split, or the new condition belongs in a guard.
Repo layout, CI, and ESLint wiring for these paths live on /implementation, not repeated on every tenet.
Counter-argument
The strongest steelman is the single-exit-point school's.3 Edsger Dijkstra and the early structured-programming literature argued that a function should have one entry and one exit, on the grounds that multiple returns make it harder to reason about the function's post-conditions. MISRA C:2012 Rule 15.5, an advisory rule that MISRA C:2025 has since disapplied, argued against early returns in safety-critical code for the same reason: every exit path is a place where invariants must hold, and multiple exits multiply the verification burden. The reply is that the “single exit” rule was written for assembly-language goto chains, not for structured early returns. The modern reading lets the function exit early when the precondition fails, which is exactly what guard clauses do.
Counter-argument retort
The single-exit-point objection looks sharp until you re-read Dijkstra in context. The 1968 “Go To Statement Considered Harmful” letter is about unbridled go to in higher-level code rather than about modern structured early returns.3 Dijkstra's argument is that arbitrary jumps make the program counter hard to reason about. An early return is not an arbitrary jump: it is a structured exit at a named precondition. The two cases are not the same and the “one entry, one exit” rule applied to the modern case produces worse code, not better.
The MISRA C residue is real and bounded. Safety-critical embedded code that has to satisfy formal-verification tools sometimes does want the single-exit shape because the verification tool reasons more easily about it. The right answer there is to follow MISRA. The right answer for application code is to follow Fowler. The two communities have different cost models for “harder to verify” and the canon picks the cost model that matches application code rather than the one for Mars-rover firmware.
The genuine residue is the “over-flattened” failure mode: a function with twelve guard clauses at the top is not easier to read than the original. It has just moved the arrow anti-pattern to the top of the function instead of the middle. The right answer there is to extract the precondition cluster into a named validator (F1 Single Responsibility): the guards become one call to a validator that returns success-or-error. The function reads as the happy path with one precondition.
The pairing makes the rule operational. The guard clause flattens. The cap (S2 Cyclomatic Caps) triggers the flattening. The validator extraction (F1 SRP) handles the over-flattened case. Three tenets, one shape: the function whose happy path reads top-to-bottom at one indentation level.
Notes
- [1]Martin Fowler: Refactoring, 2nd ed. (Addison-Wesley, 2018), ch. 10. Replace Nested Conditional with Guard Clauses: lift each abnormal case to a separate check that returns early. The body becomes the happy path.
- [2]Linus Torvalds and contributors: Linux kernel coding style §1 Indentation: “If you need more than 3 levels of indentation, you're screwed anyway, and should fix your program.”
- [3]Edsger Dijkstra: “Go To Statement Considered Harmful”, CACM 11(3), 1968. The original argument against unstructured jumps. Read in context: the case is against unbridled go to in higher-level languages (everything except, perhaps, plain machine code), not against structured early returns.
- [4]Alexander Shvets (refactoring.guru): “Replace Nested Conditional with Guard Clauses” (refactoring.guru). Worked examples with code samples in Java, C#, Python, TypeScript. The teaching version of Fowler's catalogue entry.
- [5]Kent Beck: Smalltalk Best Practice Patterns (Prentice Hall, 1996). Composed Method: every method is composed of other methods at a single level of abstraction. Guard clauses are the operational form of the pattern at the function level.
- [6]Martin Fowler: Refactoring catalogue: “Replace Nested Conditional with Guard Clauses” (refactoring.com). The web form of the catalogue entry. Its before-and-after sketch turns the nested checks of getPayAmount into three guards that each return early.