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
unless ... else is legal but style-discouraged
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.