if, elsif, and unless
In Ruby, if, elsif, and unless are how you make decisions in your code — running one block of statements instead of another based on whether some condition is true or false. They look similar to conditionals in other languages, but Ruby has its own personality here: conditionals are expressions that return a value, only nil and false are considered “falsy,” and there’s a companion keyword, unless, purpose-built for reading naturally when you want to describe a negative condition.
Overview / How it works
Every if statement evaluates a condition expression. If that expression is “truthy,” Ruby executes the code inside the if branch. If it’s “falsy,” Ruby moves on to check any elsif clauses in order, and finally falls back to the else clause if one exists. Ruby stops at the very first truthy branch it finds and skips the rest — this is called short-circuiting.
Ruby’s truthiness rule is famously simple, and different from many other languages: only nil and false are falsy. Everything else — including 0, the empty string "", and the empty array [] — is truthy. This trips up people coming from languages like C, Python, or JavaScript, where 0 or an empty collection is often falsy. In Ruby it never is.
Because almost everything in Ruby is an object (1.class is Integer, nil.class is NilClass, and even true and false are the sole instances of TrueClass and FalseClass), the truthiness check Ruby performs is really just: “is this object nil or false? If not, treat it as true.” There’s no separate “boolean coercion” step like in some languages — Ruby just asks whether the object is one of those two specific singleton objects.
unless is Ruby’s way of writing a negated if. unless condition runs its body when condition is falsy — it’s equivalent to if !condition, but it often reads more naturally in English (“unless the user is logged in, redirect to sign-in” reads better than “if the user is not logged in…”). unless supports an else clause, but — unlike if — it does not support elsif. If you need multiple branches, use if/elsif/else.
A crucial fact that surprises newcomers: if and unless are expressions, not just statements. They evaluate to the value of the last expression executed in whichever branch ran, and you can assign that value directly to a variable or pass it as an argument. If no branch runs (a falsy if with no matching elsif/else), the whole expression evaluates to nil.
Syntax
The general forms look like this:
if condition1
# runs when condition1 is truthy
elsif condition2
# runs when condition1 is falsy and condition2 is truthy
else
# runs when none of the above are truthy
end
unless condition
# runs when condition is falsy
else
# runs when condition is truthy
end
# modifier (statement) forms — no "end" needed
expression if condition
expression unless condition
# ternary — a compact if/else for simple expressions
result = condition ? value_if_true : value_if_false
| Piece | Meaning |
|---|---|
if / elsif / else |
Branches checked top to bottom; the first truthy condition’s block runs. |
unless |
Runs its body only when the condition is falsy; supports else but not elsif. |
end |
Closes a multi-line if or unless block. Not used with modifier or ternary forms. |
modifier if/unless |
A trailing form for a single statement guarded by a condition — no end. |
? : |
Ternary operator; a compact expression form of if/else for short values. |
Examples
Example 1: Grading with if/elsif/else
def grade_for(score)
if score >= 90
"A"
elsif score >= 80
"B"
elsif score >= 70
"C"
else
"F"
end
end
puts grade_for(85)
puts grade_for(95)
puts grade_for(50)
Output:
B
A
F
Each call checks the conditions top to bottom and stops at the first truthy one. For 85, score >= 90 is false, but score >= 80 is true, so "B" is the last expression evaluated and becomes the method’s return value (Ruby methods implicitly return the value of the last expression executed). For 50, none of the comparisons are true, so the else branch runs.
Example 2: unless and the modifier form
def describe_temperature(temp)
unless temp > 100
"Safe temperature: #{temp}"
else
"Danger! Temperature too high: #{temp}"
end
end
puts describe_temperature(72)
puts describe_temperature(150)
message = "Feature enabled" unless false
puts message
Output:
Safe temperature: 72
Danger! Temperature too high: 150
Feature enabled
unless temp > 100 runs its first branch precisely when temp > 100 is falsy. The last line shows the modifier form: message = "Feature enabled" unless false assigns the string because the condition (false) is falsy, so the “unless” guard lets the assignment happen.
Example 3: Truthiness in action
def check_value(value)
if value
"truthy: #{value.inspect}"
else
"falsy: #{value.inspect}"
end
end
[0, "", [], nil, false, "text"].each do |v|
puts check_value(v)
end
Output:
truthy: 0
truthy: ""
truthy: []
falsy: nil
falsy: false
truthy: "text"
This is the example every Ruby beginner needs to see once: 0, the empty string, and the empty array all take the truthy branch. Only nil and false take the falsy branch. If you want to check “is this collection empty?” you must call .empty? explicitly — the object being falsy is not the same question as the object being empty or zero.
How it works step by step
Because if/unless are expressions, you can assign their result directly instead of assigning inside every branch:
temperature = 75
weather_description =
if temperature > 85
"hot"
elsif temperature > 60
"mild"
else
"cold"
end
puts "It's #{weather_description} today"
Output:
It's mild today
Step by step: Ruby evaluates temperature > 85 first (75 > 85 is false), so it moves to the elsif. It evaluates temperature > 60 (75 > 60 is true), executes that branch, and the string "mild" becomes the value of the entire if expression — which is then assigned to weather_description. No further branches are checked once a truthy one is found, and control simply continues to the next line after end.
Common Mistakes
Mistake 1: Treating truthy-but-empty as false
def process_items(items)
if items
"Processing #{items.length} items"
else
"No items to process"
end
end
puts process_items([])
Output:
Processing 0 items
The intent was clearly to skip processing when there’s nothing to do, but [] is truthy in Ruby, so the if items check always passes for any non-nil array — even an empty one. The fix is to check emptiness explicitly:
def process_items(items)
if items.nil? || items.empty?
"No items to process"
else
"Processing #{items.length} items"
end
end
puts process_items([])
puts process_items([1, 2, 3])
Output:
No items to process
Processing 3 items
Mistake 2: Writing = instead of ==
def check_status(status)
if status = "active"
"Status: active (always true!)"
else
"Status: #{status}"
end
end
puts check_status("inactive")
Output:
Status: active (always true!)
A single typo’d = instead of == turns a comparison into an assignment. status = "active" assigns the string "active" to status and, because the assignment expression evaluates to the assigned value (a truthy string), the branch always runs — regardless of what was passed in. Ruby will even print a warning (“found `= literal’ in conditional, should be ==”) in verbose mode, but it’s easy to miss. Always double-check comparison operators:
def check_status(status)
if status == "active"
"Status: active"
else
"Status: #{status}"
end
end
puts check_status("inactive")
puts check_status("active")
Output:
Status: inactive
Status: active
Mistake 3: Trying to use elsif with unless
unless age >= 18
puts "Minor"
elsif age >= 65
puts "Senior"
else
puts "Adult"
end
unless only supports a single negated condition plus an optional else — it has no elsif form, so this raises a SyntaxError. If you find yourself wanting multiple branches, that’s a sign you should use if/elsif/else instead:
age = 70
if age < 18
puts "Minor"
elsif age >= 65
puts "Senior"
else
puts "Adult"
end
Output:
Senior
Best Practices
- Reach for
unlessonly for a single, simple negative condition with noelsif— if the logic grows more complex, switch toifwith a positive condition, which is usually easier to read. - Avoid
unless...elsewhen you can help it; a positiveif...elsereads more clearly than a negated condition followed by its opposite. - Use the modifier form (
do_something if condition) for short, single-line guards, and the full multi-line form once you have more than one statement in a branch. - Remember only
nilandfalseare falsy — use.empty?,.zero?, or.nil?when you actually mean those specific checks, not a bare truthiness test. - Take advantage of
if/unlessas expressions: assign the result directly to a variable rather than repeating an assignment inside every branch. - Use
==for comparisons and reserve bare=for assignment; if you intentionally assign inside a condition, wrap it in extra parentheses so readers (and Ruby’s warnings) know it’s deliberate.
Practice Exercises
- Write a method
ticket_price(age)that returns"free"for ages under 5,"child"for ages 5–17,"senior"for ages 65 and up, and"adult"otherwise, usingif/elsif/else. - Write a method
warn_if_low(balance)that returns"Low balance!"unlessbalanceis greater than or equal to100. Test it with50and150— what does each call return? - Given
values = [nil, false, 0, "", "hi", []], write code that iterates over the array and prints"skipped"for each falsy value and"kept: <value>"for each truthy one. Predict the output before you run it — remember onlynilandfalseare falsy.
Summary
if/elsif/elsebranches are checked top to bottom, and Ruby runs the first truthy branch it finds.- Only
nilandfalseare falsy in Ruby —0,"", and[]are all truthy. unless conditionis equivalent toif !condition; it supportselsebut neverelsif.ifandunlessare expressions that evaluate to the value of the last statement in the branch that ran (ornilif none ran), so you can assign their result directly.- Modifier forms (
expr if cond/expr unless cond) are handy for short, single-statement guards and don’t useend. - Watch for the classic pitfalls: treating truthy-but-empty values as false, typing
=instead of==, and trying to combineunlesswithelsif.
