skip to content

In Ruby, when is unless the clearer choice, and why do style guides and RuboCop's Style/UnlessElse reject unless with an else branch?

level: juniorimportance: should knowfreq 48%

answer

  1. unless means if not
  2. one negative, simple condition
  3. else under unless is a double negative
  4. positive case first
  5. no elsif after unless

basics

~20 s

unless runs its body when the condition is falsy, so it reads well for one simple negative test. Adding else makes the reader negate twice to see when each branch runs, so the convention is to rewrite it as if with the positive case first.

solid answer

~40 s

`unless cond` is exactly `if !cond`: its body runs when the condition is falsy. It reads naturally for a single, simple negative test — `return unless permit`, `notify_owner unless permit.auto_renew?`. With an `else`, the reader has to work out that the `else` runs when the condition is *truthy* — a double negative that is easy to get backwards. That is why the Ruby style guide says not to pair `unless` with `else`, and RuboCop's `Style/UnlessElse` (enabled by default) flags it and autocorrects by replacing `unless` with `if` and swapping the two bodies. `unless` also cannot take `elsif` at all — that is a syntax error. Compound or negated conditions (`unless !a`, `unless a && b`) are the other readability trap; flip them into a positive `if`.

code

ruby · 17 lines
ruby
# Flagged by Style/UnlessElse
def permit_status(permit)
  unless permit.expired?
    "valid"
  else
    "expired"
  end
end

# Autocorrected: keyword becomes if, bodies swap
def permit_status(permit)
  if permit.expired?
    "expired"
  else
    "valid"
  end
end

go deeper

for a junior

State that unless c equals if !c, show it as a guard clause, and know that an else under unless should be rewritten as if with the positive case first.

for a middle

Explain the double-negative cost of unless/else, show the mechanical rewrite that swaps bodies, and mention that unless cannot take elsif at all.

for a senior

Spot the compound and negated unless conditions that cause real bugs, rewrite them positively or behind a named predicate, and know which cops are uncontroversial (UnlessElse) versus contested (NegatedIf).

for a principal

Decide which conditional-style cops the team enforces automatically and which are left to review, weighing autocorrect churn on legacy code against readability gains.

## What unless is `unless` is Ruby's negated `if`. The language reference describes it as "the opposite of the `if` expression": the body runs when the condition is a false-value (`nil` or `false`). These two are the same program: ```ruby unless permit.expired? admit(car) end if !permit.expired? admit(car) end ``` Like `if`, `unless` is an expression: it returns the last value of the branch that ran, or `nil` when none ran. It can take an optional `then` and an optional `else`, and it has a modifier form (`admit(car) unless permit.expired?`). It **cannot** take `elsif` — the reference says so explicitly, and the parser rejects it. ## Where unless reads better than if `unless` shines when there is **one simple condition** and the code reads like an English sentence: - guard clauses: `return false unless permit` - skipping work: `send_reminder(permit) unless permit.auto_renew?` - a precondition: `raise ArgumentError, "zone required" unless zone` In each case the reader processes a single negation, and the positive name of the predicate (`auto_renew?`, not `manual_renew?`) stays readable. ## Why unless with else is discouraged Add an `else` and the reader must reason about **two** negations: ```ruby unless permit.expired? "valid" else "expired" # runs when expired? is TRUE end ``` To know when `"expired"` is returned, you negate `unless` to get "if not expired", then negate again for the `else` branch: "if expired". That is a double negative, and people routinely get it backwards when scanning. The positive rewrite removes both negations: ```ruby if permit.expired? "expired" else "valid" end ``` RuboCop implements the style guide's `no-else-with-unless` rule as **`Style/UnlessElse`**, enabled by default, with the message "Do not use `unless` with `else`. Rewrite these with the positive case first." Its autocorrection does exactly the mechanical transformation: it replaces the `unless` keyword with `if` and **swaps the two bodies**, keeping the same condition. The behaviour is unchanged; only the reading order improves. ## Other unless shapes to avoid | Shape | Problem | Preferred | RuboCop cop (1.91 default) | |---|---|---|---| | `unless c ... else ... end` | double negative | `if c` with bodies swapped | `Style/UnlessElse` (enabled) | | `unless !c` | negated negation | `if c` | `Style/NegatedUnless` (enabled) | | `if !c` with no else | negation where unless exists | `unless c` | `Style/NegatedIf` (enabled; contested) | | `unless a && b` / `unless a \|\| b` | reader must apply De Morgan's law | positive `if` with the inverted test | `Style/UnlessLogicalOperators` (disabled by default) | | `unless c ... elsif ...` | not valid Ruby | `if`/`elsif` or `case` | parse error | Two notes on that table: 1. `Style/NegatedIf` is enabled in 1.91, but its config comments that `if !x` versus `unless x` "is a preference, and `unless` is contested on its own", and it is expected to be disabled by default in the next major release. Teams differ here; `Style/UnlessElse` is far less controversial. 2. Compound conditions under `unless` are the real bug source. `unless permit.valid? && permit.paid?` runs its body when *either* check fails — correct, but readers often translate it as "when both fail". Writing `if !permit.valid? || !permit.paid?`, or better, naming the combined predicate, avoids the mental inversion. ## How to answer in an interview - `unless c` is `if !c`: use it for a single, simple, negative condition, most often as a guard clause. - Never pair it with `else`: rewrite with the positive case first; RuboCop's `Style/UnlessElse` flags it and autocorrects by swapping bodies. - It has no `elsif`; long negative chains belong in `if`/`elsif` or `case`. - Keep conditions under `unless` simple and un-negated; the whole point of `unless` is to remove a negation, not to add one.

  • How does RuboCop's Style/UnlessElse autocorrect `unless a then x else y end`?
    It replaces the `unless` keyword with `if` and swaps the two bodies, producing `if a then y else x end`. The condition is untouched, so behaviour is identical; the positive case now comes first, which removes the double negative.
  • What happens if you write elsif inside an unless expression?
    The code does not parse: Ruby's grammar gives `unless` only an optional `else`, and its language reference states that `elsif` may not be used with `unless`. When several negative cases need handling, switch to `if`/`elsif` with positive tests, or to a `case` expression.
  • Why is `unless a && b` riskier than `unless a`?
    The body runs when `a && b` is falsy, meaning when either `a` or `b` is falsy. Readers often misread it as "when neither holds". Rewriting with a positive `if` and an explicitly inverted test, or naming the combined predicate, removes the De Morgan step. RuboCop's `Style/UnlessLogicalOperators` can forbid it, but it is disabled by default.

saying these in an interview costs you the question

  • unless with else is fine because the else simply runs when the condition is false.
  • You can chain elsif after unless the same way you do after if.
  • Style/UnlessElse fixes the code by wrapping the condition in a negation.
  • unless evaluates its condition differently from if, not merely inverted.
  • unless a && b runs its body only when both a and b are falsy.