Case file S4

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.

ByAdam LewisPublished3 May 2026Revised13 Oct 2026Reading12 minVersionv1.3ConfidenceHigh
§0b

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
Copy for paste
§0c

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.

§0d

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.
The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

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.

§2

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.

Martin Fowler · Refactoring, 2nd ed. (2018), Replace Nested Conditional with Guard Clauses

The answer to that is that if you need more than 3 levels of indentation, you're screwed anyway, and should fix your program.

Linus Torvalds · Linux kernel coding style §1

Divide your program into methods that perform one identifiable task. Keep all of the operations in a method at the same level of abstraction.

Kent Beck · Smalltalk Best Practice Patterns (1996)

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.

Alexander Shvets · refactoring.guru
§3

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.

  1. 01
    Martin Fowler · 2018
    The canonical refactor. Lift each abnormal case to an early return. The body becomes the happy path. Indentation drops by one level for every conversion.
  2. 02
    Alexander Shvets · 2014 onwards
    The teaching form of Fowler's catalogue entry. Worked examples in Java, C#, Python, TypeScript. The before-and-after pattern is unambiguous.
  3. 03
    Linus Torvalds · 1995 onwards
    Three 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.
  4. 04
    Kent Beck · 1996
    The larger pattern. Single level of abstraction per method. The guard-clause shape falls out as the operational form of the rule.
  5. 05
    Robert C. Martin · 2008
    Same instinct, different vocabulary. Small functions, one indentation level, abnormal cases lifted to guards. The pattern survived the editorial controversy because the shape is right.
  1. 06
    Edsger Dijkstra · 1968
    The 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.
  2. 07
    MISRA Consortium · 2012
    Safety-critical embedded code that must satisfy formal-verification tools sometimes requires single-exit functions. Bounded counter-position. Application code has a different cost model.
  3. 08
    ESLint maintainers · 2013 onwards
    The 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.
  4. 09
    David Thomas & Andrew Hunt · 1999, 2019
    Crash 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.
  5. 10
    ESLint maintainers · 2013 onwards
    Catches 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.
  6. 11
    Pylint project · 2003 onwards
    Python implementation of the depth cap. Default 5. Tighten to 3 to match the editorial position; Ruff inherits the rule under the same identifier.
  7. 12
    Astral / Charlie Marsh · 2022 onwards
    Modern Python surface for the same cap. Single binary, single TOML block. The right choice for new Python projects.
  8. 13
    PMD project · 2002 onwards
    Java implementation of the depth cap. Default 3. The canonical Java lint surface for the rule.
  9. 14
    PHP_CodeSniffer project · 2006 onwards
    PHP implementation of the depth cap. Default 5. Tighten to 3 for parity with the JavaScript and Python surfaces.
  10. 15
    Sandi Metz · 2015
    Talk 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.
  11. 16
    Joshua Kerievsky · 2004
    Restatement 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.

§4

Examples

Viewing: TypeScript.
Avoid

Before one function checking four concerns. Pyramid of doom shape.

Filefeed-hedgehog.ts
function canFeedHedgehog(hedgehog: Hedgehog | null, food: Food): boolean {
  if (hedgehog) {
    if (!hedgehog.isHibernating) {
      if (hedgehog.weightG > 600) {
        if (food.isSafe) {
          return true;
        }
      }
    }
  }
  return false;
}
Prefer

After each concern is its own predicate. The orchestrator has one guard, then combines.

Filefeed-hedgehog.ts
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;
§4b

Enforcement

Viewing: TypeScript.

Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
max-depthESLint coreindentation depth above the cap. The lever Linus names directly. Three is the kernel's number; the Prickles repo agrees.
no-else-returnESLint corethe if-return-else-something pattern that guard clauses retire. With allowElseIf: false, the rule rewrites if-else-if chains into early returns.
no-lonely-ifESLint corethe lonely if-inside-else that should have been an else-if. The micro-cousin of the arrow anti-pattern.
unicorn/no-negated-conditioneslint-plugin-unicornnegated conditions that read if (!X) { … } else { … }: usually the guard you want to lift, expressed inside-out.
unicorn/no-nested-ternaryeslint-plugin-unicornnested ternary expressions: the inline form of the arrow anti-pattern. Drives the rewrite into named guards.
sonarjs/no-collapsible-ifeslint-plugin-sonarjsan 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',
  }
});
§4c

AI rules

File.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.

§5

Counter-argument

Counter

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.

§6

Counter-argument retort

Reply

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.

§7

Notes

  1. [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. [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. [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.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-10-13