Skip to content
aipatterns.dev

Refactoring techniqueSimplifying Conditional Expressions

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.

Also known as
Early Return

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

TypeScript
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

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

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.

Treats these smells

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.