# Replace Nested Conditional with Guard Clauses

> Handle special and invalid cases first with early returns so the normal path runs flat at the end of the function.

- kind: technique
- category: simplifying-conditional-expressions
- aliases: Early Return
- canonical: https://aipatterns.dev/refactoring/techniques/replace-nested-conditional-with-guard-clauses
- inspired-by: https://refactoring.guru/replace-nested-conditional-with-guard-clauses

## Problem

A function wraps its main work in several levels of `if/else`. The normal case is buried at the deepest indentation, and the `else` branches that handle edge cases are far from the conditions that trigger them.

## Solution

**Check each special case at the top of the function and return (or throw) immediately. Leave the normal case as unindented code at the bottom.**

## Before

```ts
function issueToken(req: LoginRequest, user: User | null): AuthResult {
  let result: AuthResult;
  if (user) {
    if (!user.locked) {
      if (verifyPassword(req.password, user.passwordHash)) {
        if (user.mfaEnabled && !req.otp) {
          result = { kind: "mfa-required" };
        } else {
          result = { kind: "ok", token: signJwt(user.id) };
        }
      } else {
        result = { kind: "denied", reason: "bad-credentials" };
      }
    } else {
      result = { kind: "denied", reason: "locked" };
    }
  } else {
    result = { kind: "denied", reason: "bad-credentials" };
  }
  return result;
}
```

## After

```ts
function issueToken(req: LoginRequest, user: User | null): AuthResult {
  if (!user) return { kind: "denied", reason: "bad-credentials" };
  if (user.locked) return { kind: "denied", reason: "locked" };
  if (!verifyPassword(req.password, user.passwordHash)) {
    return { kind: "denied", reason: "bad-credentials" };
  }
  if (user.mfaEnabled && !req.otp) return { kind: "mfa-required" };

  return { kind: "ok", token: signJwt(user.id) };
}
```

## Why

- Each precondition sits next to its outcome, so you read the rules top to bottom.
- The success path is the last line, at the lowest indentation, which is where readers look for it.
- Adding a new rejection rule is one new line, not another nesting level.

## Steps

1. Pick the outermost condition whose `else` branch is a special case. Invert the condition, return the `else` result at the top, and unwrap the `then` body by one level. Run the tests.
2. Repeat for the next level. Keep the original order of checks, because later checks may rely on earlier ones (here `user` must be non-null before `user.locked`).
3. When the only remaining code is the normal path, remove the result variable and return directly. Run the tests.
4. If several guards return the same value, consider [Consolidate Conditional Expression](https://aipatterns.dev/refactoring/techniques/consolidate-conditional-expression.md). Keep them separate when the reasons differ.

## Watch out

- Do not use guards when both branches are equally normal, such as choosing between two pricing formulas. A plain `if/else` (or a ternary) states that the cases are peers.
- Cleanup code at the end of the function will be skipped by early returns. Move it into `finally`, a `using` block or a wrapper before adding guards.
- Inverting conditions is error prone. `!(a && b)` is `!a || !b`; check each inversion with a test.
- Timing-sensitive checks (password comparison, user lookup) may need to run in the same order and count for security reasons. Preserve order exactly.
- The inverse direction, wrapping returns back into nested blocks, has no named technique and is rarely useful.

## Related

- Shortens [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md).
- Often applied after [Remove Control Flag](https://aipatterns.dev/refactoring/techniques/remove-control-flag.md) and [Decompose Conditional](https://aipatterns.dev/refactoring/techniques/decompose-conditional.md).

## Agent checklist

- [ ] The guarded cases are exceptional; the remaining code is the single normal path.
- [ ] Every inverted condition is logically equivalent to the original `else` trigger.
- [ ] Check order is unchanged, so null guards still protect later checks.
- [ ] No cleanup or logging at the function's end is now skipped.
