rubyguides

Ruby keyword: unless

unless expr then body [else body] end  |  body unless expr

Ruby’s unless keyword runs a branch when its test expression is false. It is the inverse of if, with the same truth rules and the same expression-level return value. Once you internalize that unless is just if with the condition flipped, the only things left to learn are the small set of rules the keyword does not share with if: no elsif, a community style preference against else, and a couple of parser traps.

The official Ruby control-expressions page lists unless alongside if, case, and while as one of the four core conditional and looping forms. The grammar below comes from that page.

Syntax forms

Every block form closes with end. The condition runs first; if it is false, the body runs and its last expression becomes the value of the whole unless expression. If the condition is true, no branch runs and the expression returns nil.

Simple unless

n = 0

unless n.zero?
  puts "nonzero"
end
# => nil   # "nonzero" is not printed because 0.zero? is true

n.zero? is true, so the body is skipped, and the unless expression itself returns nil. There is no hidden else nil clause; an unless with no else returns nil exactly when the condition is truthy, same as a no-else if.

unless with else

signed_in = false

unless signed_in
  "show login"
else
  "show dashboard"
end
# => "show login"

else is allowed with unless. It runs when the condition is truthy, which is the opposite of the main branch. Community style generally discourages this form because it forces the reader to mentally flip the condition; see the gotcha section below for the standard rewrite.

Modifier unless

n = 1
n += 1 unless n.zero?
n
# => 2

Modifier unless puts the body on the left and the test on the right. It is a statement form, which matters for one of the gotchas below.

# Also valid: log on a condition
log_event("created") unless Rails.env.test?

then is optional on a newline

then is the separator between the test and the body when they share a line. On its own line the body is unambiguously the next expression, so the separator becomes optional.

unless signed_in then "guest" else "member" end
# same as the newline form

Both compile to the same thing, but the newline form is what almost every Ruby style guide recommends.

elsif is not allowed with unless

This is the rule that catches people coming from if:

unless n == 1
  "not one"
elsif n == 2
  "two"
end
# SyntaxError: unexpected keyword 'elsif'

Reach for case, or flip the condition and write an inverted if/elsif/else chain. That second approach is what style guides usually want anyway.

Truthy and falsy values

unless uses the same truth rules as if: only nil and false are false. Everything else, including 0, "", [], and any object that has not explicitly opted out, is truthy.

unless 0       then "yes" else "no" end
# => "yes"
unless ""      then "yes" else "no" end
# => "yes"
unless []      then "yes" else "no" end
# => "yes"
unless nil     then "yes" else "no" end
# => "yes"
unless false   then "yes" else "no" end
# => "yes"

A common consequence: an empty array is truthy, so unless items does not check whether the array has any elements. Use items.empty? or items.any? to test contents.

items = []
unless items
  puts "has items"
else
  puts "empty"
end
# prints "has items" (empty array is still truthy)

Unless expression return values

Because unless is an expression, you can assign its result, return it from a method, or pass it to another method. The value is the last expression evaluated in the branch that ran, or nil if no branch ran.

def fee(age)
  unless age < 12
    10
  else
    0
  end
end

fee(8)    # => 0
fee(30)   # => 10

def label(n)
  unless n == 0
    "nonzero"
  end
end

label(0)  # => nil
label(1)  # => "nonzero"

The body that did not run does not contribute a value. A method that would be a chain of return statements in other languages reads as a single value expression here.

Common gotchas

Modifier unless is a statement, not an expression

You cannot pass a modifier unless directly to a method as an argument:

puts(1 unless true)   # SyntaxError

Wrap it in an extra pair of parens to make it an expression. The outer parens belong to puts; the inner parens force the parser to evaluate the modifier unless as an expression that resolves to either the body or nil.

puts((1 unless true))
# prints 1

The parser-ordering trap with assignment in the test

The same trap that hits if hits unless exactly the same way. When you put a fresh local variable on both sides of a modifier unless, the parser only sees the assignment while it is scanning the test expression. By the time the body runs, that variable has not been declared in scope yet, so the lookup falls back to a method call:

p a unless a = 0.zero?
# raises NameError: undefined local variable or method `a'

The parser scans left to right. The body p a tells it that a is a method call, so when the body runs, a is looked up as a method. The assignment in the test only marks a as a local in the surrounding scope, which is too late for the body’s lookup. Assign first, then test.

a = 0.zero?
p a unless a
# prints false

else with unless is allowed by the language. RuboCop’s Style/UnlessElse cop is on by default in most configs and flags the pattern below whenever the bodies are simple enough that flipping the condition would read clearly without forcing the reader to invert it in their head:

# bad (per Style/UnlessElse)
unless foo_bar.nil?
  do_something
else
  do_other
end

The recommended rewrite is to flip the condition to its positive form, which reads more directly because the reader no longer has to invert the meaning of the test on the fly. The same linter that flagged the original will leave the inverted if chain alone:

# good
if foo_bar.nil?
  do_other
else
  do_something
end

Ruby itself emits no warning for unless ... else. This is a community and linter convention, not a language rule, so ruby -w will stay silent. The reason behind the convention is that an unless ... else chain forces the reader to mentally flip the condition in their head, which costs more than the extra characters saved.

unless is not until

Beginners sometimes conflate the two because the words look similar. unless is a conditional expression: the body runs once if the test is false. until is a loop: the body runs repeatedly while the test is false. They are completely different keywords.

Compound conditions follow De Morgan

unless a && b
  ...
end

is equivalent to if !(a && b) and to if !a || !b. There is no elsif chain hiding inside the unless, and no special “unless-not” operator. Write the form that reads most naturally and trust the equivalence.

Idiomatic patterns

Guard clauses

Modifier unless is the canonical guard at the top of a method or block:

return unless user&.admin?

Early returns

Pairing unless with the nil? predicate is a common way to bail out before doing real work. The pattern pays off most in long methods, where each guard shortens the indented main path and keeps the happy flow at the leftmost indentation level:

def process(order)
  return if order.nil?
  return unless order.paid?
  # ...
end

One-liner side effects

When the body is a single side effect and the test is genuinely simpler in its negative form, modifier unless reads well. Reach for it when the body is a method call that is itself a complete statement, like logging a line or emitting an event you already have a wrapper for:

log_event("created") unless Rails.env.test?

If the predicate has a natural positive inverse method, prefer the positive form. return if user.nil? usually reads more directly than return unless user, and most style guides recommend it.

See also