if, elsif, and unless

Published by Programming Line in Ruby. Last updated .

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 unless only for a single, simple negative condition with no elsif — if the logic grows more complex, switch to if with a positive condition, which is usually easier to read.
  • Avoid unless...else when you can help it; a positive if...else reads 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 nil and false are falsy — use .empty?, .zero?, or .nil? when you actually mean those specific checks, not a bare truthiness test.
  • Take advantage of if/unless as 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

  1. 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, using if/elsif/else.
  2. Write a method warn_if_low(balance) that returns "Low balance!" unless balance is greater than or equal to 100. Test it with 50 and 150 — what does each call return?
  3. 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 only nil and false are falsy.

Summary

  • if/elsif/else branches are checked top to bottom, and Ruby runs the first truthy branch it finds.
  • Only nil and false are falsy in Ruby — 0, "", and [] are all truthy.
  • unless condition is equivalent to if !condition; it supports else but never elsif.
  • if and unless are expressions that evaluate to the value of the last statement in the branch that ran (or nil if 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 use end.
  • Watch for the classic pitfalls: treating truthy-but-empty values as false, typing = instead of ==, and trying to combine unless with elsif.