---
title: "Defensive Overkill"
type: "ai-smell"
slug: "defensive-overkill"
url: "http://localhost:3000/en/ai-smells/defensive-overkill.md"
category: "Maintenance"
description: "AI assistants hedge against failures that can't happen, wrapping already-safe code in redundant null checks, dead guard clauses, and catch-all try/except blocks that add complexity without adding safety."
---
# Defensive Overkill

> AI assistants hedge against failures that can't happen, wrapping already-safe code in redundant null checks, dead guard clauses, and catch-all try/except blocks that add complexity without adding safety.

## Signs and Symptoms

A reviewer spots Defensive Overkill when a function spends more lines guarding against impossible states than doing actual work. Tell-tale signs:

* **Guards for conditions the type system already guarantees** — null/undefined checks on a non-nullable typed parameter, `typeof x === "string"` on a `string`.
* **Duplicate or subsumed guards** — `if (!user) return` immediately followed by `if (user === null || user === undefined) return`.
* **Catch-all `try/catch` around code that cannot throw**, often swallowing the error or just re-throwing it ("just in case").
* **Logic for "phantom" edge cases** — branches that handle inputs no caller can produce. OX Security found AI routinely adds "logic for imaginary edge cases."
* **Defensive blocks copy-pasted** rather than extracted, so the same guard appears in five places.

```ts
// User is a NON-nullable type: { profile: { name: string } }
function getDisplayName(user: User): string {
  if (!user) return "Unknown";                         // dead: user is non-nullable
  if (user === null || user === undefined) return "?"; // duplicate, also dead
  try {
    if (user.profile && typeof user.profile.name === "string") { // type already guarantees this
      const name = user.profile.name;
      if (name.length > 0) {
        return name.trim() !== "" ? name.trim() : "Unknown";
      }
    }
    return "Unknown";
  } catch (e) {
    console.error("could not get name", e); // this block can't throw
    return "Unknown";
  }
}

```

Nine lines of hedging wrap one line of intent. The contradiction noted by reviewers is that the _same_ model often omits the one check that matters (e.g. validating untrusted external input) while over-guarding internal calls — defensiveness scattered by vibe, not by threat model.

## Reasons for the Problem

### Why models produce it

* **Next-token risk-aversion / RLHF "helpfulness."** Models are tuned to look thorough and avoid being "wrong." Emitting an extra guard or `try/catch` is low-risk for the next-token objective and reads as conscientious, so it is over-sampled. A reviewer at scale noted repeated `if (array && array.length > 0)` chains are "a sign the model isn't fully confident in the code flow" — the guard is a hedge against the model's own uncertainty.
* **No whole-repo dataflow context.** The model can't see that a caller already validated the argument, or that a TypeScript type makes a branch unreachable. arXiv 2509.20491 (_AI-Specific Code Smells_) shows LLMs "struggle with code smells that are flow-dependent or value-sensitive" — when they can't reason about flow, they default to guarding everything locally.
* **Security-prompt cargo-culting.** Research on secure-code prompting found that when asked to "make it secure," LLMs "add try-catch blocks as a standalone security measure without other security enhancements... typically when they cannot identify specific vulnerabilities." Defensiveness substitutes for understanding.
* **Training-data bias toward verbose, tutorial-style code** that demonstrates every check for pedagogy, plus a refusal to refactor: OX Security found **avoidance of refactors in 80–90%** of AI code and **over-specification in 80–90%** — the model adds, it rarely removes.

### Why it hurts

* **Maintainability & review load.** A taxonomy of LLM-Python inefficiencies (arXiv 2503.06327) catalogs _redundant input validation_, _over-defensive error handling_, and _unnecessary guard conditions_, concluding these "defensive patterns add complexity without proportional safety benefits." Reviewers must read every dead branch to confirm it _is_ dead.
* **Correctness, not just clutter.** Catch-all blocks that swallow or generically handle errors hide real failures; a `try/catch` that returns a default turns a bug into silent wrong behavior. The guards also create dead branches that tests dutifully cover, inflating coverage with meaningless tests (another OX finding).
* **Tech-debt compounding.** GitClear's 2025 analysis found refactoring's share of changed lines fell from **25% (2021) to under 10% (2024)** while copy/pasted lines rose to **12.3%** and duplicate blocks grew \~8x. Defensive boilerplate is exactly the kind of code that gets cloned instead of extracted, and cloned blocks correlate with **15–50% more defects**.
* **Higher cognitive complexity** per function makes the real logic harder to find, slowing every future change.

## Treatment

### Review & prompting tactics

* **Make the contract explicit so guards become provably unnecessary.** Tell the model: _"`user` is non-nullable and already validated by the caller — do not re-check it."_ Lean on types: with `strictNullChecks` on, `@typescript-eslint/no-unnecessary-condition` will flag dead guards for you.
* **Prompt for proportional defense, not blanket defense.** Ask: _"Only handle errors you can describe a concrete trigger for. Validate untrusted/external input at the boundary; trust internal calls."_ This separates real input validation (keep it) from phantom edge cases (delete them).
* **Require the model to run the linter/type-checker and remove what it flags** before returning code — close the loop the way agent practitioners recommend (linters, type checkers, tests as automated feedback signals).
* **Ask it to delete, not just add:** _"Refactor for the minimum code that satisfies the spec; remove unreachable branches and catch blocks that can't fire."_ This counters the documented "avoidance of refactors" bias.

### The refactor

Name the classic moves: **Remove Dead Code**, **Consolidate Conditional Expression**, **Replace Nested Conditional with Guard Clauses**, and (for input that _does_ need checking) **Introduce Assertion / validate once at the boundary** instead of repeatedly. Where the same guard was copy-pasted, **Extract Function**.

```ts
// AFTER — type guarantees non-null; validate once, no phantom catch
function getDisplayName(user: User): string {
  const name = user.profile.name.trim();
  return name || "Unknown";
}

```

If a value genuinely _is_ untrusted, validate it **once** at the edge and let the rest of the code trust the now-narrowed type:

```ts
// boundary: parse/validate untrusted input a single time
const user = UserSchema.parse(rawInput); // throws on bad data, here, on purpose
// ...everything downstream takes a validated `User` and needs no re-guarding

```

Rule of thumb for reviewers: every guard and every `catch` must answer _"what concrete caller or input triggers this?"_ If the answer is "nothing — just in case," delete it.

## Detected by

- **typescript-eslint** `@typescript-eslint/no-unnecessary-condition` — no-unnecessary-condition (https://typescript-eslint.io/rules/no-unnecessary-condition/)
- **ESLint** `no-useless-catch` — no-useless-catch (https://eslint.org/docs/latest/rules/no-useless-catch)
- **SonarSource (SonarQube / eslint-plugin-sonarjs)** `RSPEC-2589 — Boolean expressions should not be gratuitous (always-true/false conditions)` — no-gratuitous-expressions (https://rules.sonarsource.com/javascript/RSPEC-2589/)
- **SonarSource (SonarQube / eslint-plugin-sonarjs)** `RSPEC-3776 — Cognitive Complexity (nested defensive guards inflate it; proxy detector)` — cognitive-complexity (https://rules.sonarsource.com/javascript/RSPEC-3776/)
