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.1Refactoring, 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. A function takes a value, checks it is not null, opens a brace; checks it is the right type, opens another; checks the user has permission, opens another; and the actual work 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.2Linux kernel coding style §1 Indentation: “If you need more than three levels of indentation, you're screwed anyway, and should fix your program.” 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 what makes the function 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. Three 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.
- Cyclomatic complexity 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.1Refactoring, 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. 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.
The rule the move enforces is older than the catalogue. Linus Torvalds put it in three sentences in the Linux kernel coding style:2Linux kernel coding style §1 Indentation: “If you need more than three levels of indentation, you're screwed anyway, and should fix your program.” “If you need more than three 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 carries the canonical worked example with code samples in multiple languages.4“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. 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 deeper underlying pattern.5Smalltalk 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. 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
Conditional expressions take two forms. In the first form, both legs of the conditional are normal behaviour, so I use an if-then-else construct. In the second form, one of the legs is unusual. In this case, that leg should exit the routine — that's a guard clause.
If you need more than three levels of indentation, you're screwed anyway, and should fix your program.
Compose methods so that they are at a single level of abstraction. The reader can either understand the message names without looking at any of them, or look at the implementation of one and find that it talks at the same level as its caller.
You have a group of nested conditionals and it is hard to determine the normal flow of code execution. Replace the nested conditionals with guard clauses for all the special cases.
Evidence
Twenty 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 contributor guidelines bounce patches that break the rule. Thirty years of working under the rule across a million-line C codebase.
- 04The deeper 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 underlying shape is right.
Sixteen sources, two stances. The supporters cluster on the move itself: Fowler, Beck, refactoring.guru, Linus. The qualifiers (the single-exit-point school, MISRA C's strict reading) carry the “multiple returns are harder to reason about” argument. There is no substantive opposition; 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;
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. Forces the rewrite into named guards. |
| sonarjs/no-collapsible-if | eslint-plugin-sonarjs | two adjacent ifs that could be combined with `&&`. One guard for two conditions; the rule fixes the trivial case automatically. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import unicorn from 'eslint-plugin-unicorn';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { unicorn },
rules: {
'max-depth': ['warn', 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',
}
});AI rules
.cursor/rules/s4-guard-clauses.mdc---
description: Prickles S4 — Guard Clauses
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## 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 the lever that forces 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“Go To Statement Considered Harmful”, CACM 11(3), 1968. The original argument against unstructured jumps. Read in context: the case is about goto-chains in assembly-style code, not structured early returns. 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's strict reading still bans 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 “Goto Considered Harmful” letter is about goto chains in assembly-style code, not about modern structured early returns.3“Go To Statement Considered Harmful”, CACM 11(3), 1968. The original argument against unstructured jumps. Read in context: the case is about goto-chains in assembly-style code, not structured early returns. 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 one that matches application code, not the one that matches 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) forces 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 three 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 about goto-chains in assembly-style code, not structured early returns.