Conditional Selection #

Every useful program makes decisions — running this code if a condition is met, skipping that block if it isn’t. Ruby provides a wide range of ways to express these decisions: from the classic if-elsif-else, to unless which reads like an English sentence, to case/when which is far more powerful than switch in other languages because it can match by class, regex, range, and even custom conditions. What makes conditional selection in Ruby interesting is that it’s all expressions — meaning if, unless, and case all return values and can be used directly in assignments. Understanding this nuance makes Ruby code feel more concise and expressive.

if — The Basic Form #

if is the foundation of conditional selection. The code block inside it only executes if the condition is truthy.

stock = 5

if stock > 0
  puts "Product available"
end

Because if is an expression, it returns the value of the last executed line — or nil if the condition isn’t met:

score = 85

message = if score >= 60
           "Passed"
         end

puts message.inspect   # => "Passed"

# If the condition isn't met, if returns nil
message2 = if score < 60
             "Failed"
           end

puts message2.inspect  # => nil

Because if returns a value, you can use it directly in an assignment — an idiom often seen in clean Ruby code:

# ANTI-PATTERN: assignment in every branch — repetitive
grade = nil
if score >= 90
  grade = "A"
elsif score >= 80
  grade = "B"
elsif score >= 70
  grade = "C"
else
  grade = "D"
end

# CORRECT: if as an expression — one assignment, clean
grade = if score >= 90 then "A"
         elsif score >= 80 then "B"
         elsif score >= 70 then "C"
         else "D"
         end

if-elsif-else — Multi-Branch Selection #

Use elsif (note: not elseif or else if) to add additional condition branches:

def age_category(age)
  if age < 0
    raise ArgumentError, "Age cannot be negative"
  elsif age < 13
    "child"
  elsif age < 18
    "teenager"
  elsif age < 60
    "adult"
  else
    "senior"
  end
end

puts age_category(8)    # => "child"
puts age_category(15)   # => "teenager"
puts age_category(35)   # => "adult"
puts age_category(65)   # => "senior"

Ruby evaluates each condition from top to bottom and stops at the first branch that matches — subsequent branches aren’t checked. This means the order of elsif matters:

# ANTI-PATTERN: wrong condition order — broader condition on top
def discount_level(total)
  if total > 100_000     # this is always true if total > 500_000 is also true
    "Bronze - 5%"
  elsif total > 500_000  # never reached for total > 500_000!
    "Gold - 15%"
  elsif total > 1_000_000
    "Platinum - 20%"
  end
end

# CORRECT: most specific (strictest) condition on top
def discount_level(total)
  if total > 1_000_000
    "Platinum - 20%"
  elsif total > 500_000
    "Gold - 15%"
  elsif total > 100_000
    "Bronze - 5%"
  else
    "No discount"
  end
end

unless — The Opposite of if #

unless executes a code block when its condition is not met — it’s an if not that’s easier to read in many situations.

out_of_stock = false

unless out_of_stock
  puts "Add to cart"
end

# Equivalent to:
if !out_of_stock
  puts "Add to cart"
end

unless is most powerful when the condition being checked is easier to phrase negatively:

# unless reads more naturally than if !
unless user.blocked?
  allow_login(user)
end

unless balance < minimum_balance
  process_withdrawal(amount)
end

unless ENV["RAILS_ENV"] == "production"
  puts "[DEBUG] Development mode active"
end
Don’t use unless with conditions containing negation or the || and && operators — the result is hard to read and easy to misread. Simple rule: if you need unless with || or &&, use if with a positively rephrased condition instead.
# ANTI-PATTERN: unless with negation — two negations in one expression
unless !user.active?          # means: "if the user is active" — just use if!
  process(user)
end

# ANTI-PATTERN: unless with ||
unless a.nil? || b.nil?       # confusing — hard to parse mentally
  combine(a, b)
end

# CORRECT: rewrite with if
if user.active?
  process(user)
end

if a && b
  combine(a, b)
end

Postfix Modifiers — One-Line if and unless #

Ruby allows writing if and unless at the end of a line — after the code you want to condition. This is called a postfix modifier and is one of Ruby’s most expressive idioms.

# if modifier
puts "Access granted" if user.admin?
send_email(user) if user.email_verified?
log.warn("Low balance") if balance < minimum_balance

# unless modifier
redirect_to(root_path) unless user.logged_in?
raise ArgumentError, "Name cannot be empty" unless name.present?

The postfix modifier is most appropriate for:

When to use the postfix modifier:
  ✓ Guard clauses — rejecting invalid conditions at the start of a method
  ✓ One-line expressions that don't need an else
  ✓ Code that reads naturally from left to right
  ✓ Optional logging and debugging

  ✗ Conditions that need a multi-line block
  ✗ Complex conditions that make left-to-right reading difficult
  ✗ When there's an else — use a regular if-else
# ANTI-PATTERN: complex logic in a postfix modifier
save_data(process(transform(input))) if valid?(input) && !locked? && user.has_permission?(:write)

# CORRECT: split when the condition is complex
return unless valid?(input)
return if locked?
return unless user.has_permission?(:write)

save_data(process(transform(input)))

Guard Clauses and Early Return #

A guard clause is the pattern of using if/unless modifiers at the start of a method to handle edge cases as early as possible, before entering the main logic. It’s one of the most recommended patterns in Ruby because it reduces nesting and keeps the main logic clean.

# ANTI-PATTERN: deep nesting that buries the main logic
def process_payment(user, order, card)
  if user
    if user.active?
      if order
        if order.unpaid?
          if card && card.valid?
            # main logic here — 5 levels deep!
            charge(card, order.total)
            order.mark_paid
            send_notification(user, order)
          else
            "Invalid card"
          end
        else
          "Order already paid"
        end
      else
        "Order not found"
      end
    else
      "User inactive"
    end
  else
    "User not found"
  end
end

# CORRECT: guard clauses — handle edge cases on top, main logic below
def process_payment(user, order, card)
  return "User not found"     unless user
  return "User inactive"      unless user.active?
  return "Order not found"    unless order
  return "Order already paid" unless order.unpaid?
  return "Invalid card"       unless card&.valid?

  # main logic — no nesting, easy to read
  charge(card, order.total)
  order.mark_paid
  send_notification(user, order)
  "Payment successful"
end
flowchart TD
    A[Enter method] --> B{user exists?}
    B -- No --> R1["return: User not found"]
    B -- Yes --> C{user.active?}
    C -- No --> R2["return: User inactive"]
    C -- Yes --> D{order exists?}
    D -- No --> R3["return: Order not found"]
    D -- Yes --> E{order unpaid?}
    E -- No --> R4["return: Order already paid"]
    E -- Yes --> F{card valid?}
    F -- No --> R5["return: Invalid card"]
    F -- Yes --> G[Main logic\ncharge, mark paid, notify]
    G --> H["return: Payment successful"]

case/when — Powerful Pattern Matching #

case/when in Ruby is far more powerful than switch/case in most other languages. when uses the === (triple equals) operator for matching — and every class can define its own ===. This means when can match by value, class, range, regex, Proc, and more.

Basic Value Matching #

day = "Wednesday"

message = case day
          when "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
            "Workday"
          when "Saturday", "Sunday"
            "Weekend"
          else
            "Unknown day"
          end

puts message   # => "Workday"

Matching by Range #

score = 78

grade = case score
        when 90..100 then "A — Excellent"
        when 80..89  then "B — Good"
        when 70..79  then "C — Fair"
        when 60..69  then "D — Poor"
        else              "E — Failing"
        end

puts grade   # => "C — Fair"

Matching by Class #

This is the case/when feature that most distinguishes Ruby from other languages — when can match by object type:

def describe(value)
  case value
  when Integer then "Integer: #{value}"
  when Float   then "Float: #{value}"
  when String  then "String: \"#{value}\""
  when Array   then "Array with #{value.length} elements"
  when Hash    then "Hash with #{value.size} keys"
  when Symbol  then "Symbol: :#{value}"
  when NilClass then "Empty value (nil)"
  when TrueClass, FalseClass then "Boolean: #{value}"
  else "Unknown type: #{value.class}"
  end
end

puts describe(42)           # => Integer: 42
puts describe(3.14)         # => Float: 3.14
puts describe("hello")      # => String: "hello"
puts describe([1, 2, 3])    # => Array with 3 elements
puts describe(nil)          # => Empty value (nil)
puts describe(true)         # => Boolean: true

Matching by Regex #

def classify_input(text)
  case text
  when /\A\d+\z/
    "Whole number: #{text.to_i}"
  when /\A\d+\.\d+\z/
    "Decimal number: #{text.to_f}"
  when /\A[\w.+-]+@[\w-]+\.[a-z]{2,}\z/i
    "Email address: #{text}"
  when /\Ahttps?:\/\//
    "URL: #{text}"
  when /\A(\+62|0)\d{9,12}\z/
    "Indonesian phone number: #{text}"
  else
    "Plain text: #{text}"
  end
end

puts classify_input("42")                    # => Whole number: 42
puts classify_input("[email protected]")      # => Email address: ...
puts classify_input("https://ruby-lang.org") # => URL: ...
puts classify_input("08123456789")           # => Indonesian phone number: ...

case Without an Expression — an if-elsif Replacement #

case can be used without an expression after it, acting as a cleaner replacement for if-elsif:

def check_conditions(temperature, humidity, wind)
  case
  when temperature > 38 && humidity > 80
    "Danger: extreme heat and humidity"
  when temperature > 35
    "Warning: very high temperature"
  when wind > 80
    "Warning: strong winds"
  when temperature < 10
    "Warning: very low temperature"
  else
    "Normal conditions"
  end
end

Capturing with then and Local Variables #

Since Ruby 3.x, case/in (pattern matching) allows extracting values from data structures directly:

# Pattern matching with case/in (Ruby 3.0+)
response = { status: 200, data: { user: { name: "Rani", role: :admin } } }

case response
in { status: 200, data: { user: { name: String => name, role: :admin } } }
  puts "Admin login: #{name}"
in { status: 200, data: { user: { name: String => name } } }
  puts "User login: #{name}"
in { status: 401 }
  puts "Unauthorized"
in { status: 404 }
  puts "Not found"
in { status: (500..) }
  puts "Server error"
end
# => "Admin login: Rani"

# Pattern matching for Arrays
coordinates = [10, 20, 30]

case coordinates
in [x, y]
  puts "2D: (#{x}, #{y})"
in [x, y, z]
  puts "3D: (#{x}, #{y}, #{z})"
end
# => "3D: (10, 20, 30)"

The Ternary Operator #

The ternary operator (? :) is a compact if-else form for one-line expressions that return a value:

age    = 20
status = age >= 18 ? "adult" : "minor"
puts status   # => "adult"

# Idiomatic uses
label  = balance >= 0 ? "Credit" : "Debit"
color  = error? ? :red : :green
prefix = count == 1 ? "item" : "items"
# ANTI-PATTERN: ternary for side effects
condition ? puts("yes") : puts("no")

# ANTI-PATTERN: nested ternary
result = a > 0 ? (b > 0 ? "++ positive" : "+- mixed") : "negative"

# CORRECT: ternary only for expressions that return a value
label = active? ? "Active" : "Inactive"

# CORRECT: if-elsif for multi-level conditions
result = if a > 0 && b > 0
           "both positive"
         elsif a > 0
           "only a positive"
         else
           "a not positive"
         end

then — Compact Writing #

The then keyword allows writing if and when on a single line without using a newline as a separator:

# With then in if
if x > 0 then puts "positive" end

# More often used in case/when for one-liners:
case status
when :active   then "Active user"
when :pending  then "Awaiting verification"
when :suspended then "Account suspended"
else                "Unknown status"
end

Conditional Selection as an Expression in Other Contexts #

Because if, unless, and case are expressions, they can be used anywhere a value is accepted — including as method arguments, hash values, and array elements:

# As a direct method argument
log.info(if debug? then "Debug mode active" else "Production mode" end)

# In a hash
config = {
  timeout: if production? then 30 else 5 end,
  log_level: (debug? ? :debug : :info)
}

# As a method return value — Ruby automatically returns the last expression
def status_label(code)
  case code
  when 200 then "OK"
  when 201 then "Created"
  when 400 then "Bad Request"
  when 401 then "Unauthorized"
  when 404 then "Not Found"
  when 500 then "Internal Server Error"
  else "Unknown (#{code})"
  end
  # no 'return' needed — case returns its last value
end

puts status_label(404)   # => "Not Found"
puts status_label(999)   # => "Unknown (999)"

Choosing the Right Construct #

flowchart TD
    A[Need conditional selection] --> B{How many branches?}
    B --> C["One branch\n(only if true)"]
    B --> D["Two branches\n(if and else)"]
    B --> E["Many branches"]
    C --> F{Positive or\nnegative condition?}
    F --> G["Positive → if modifier\nputs x if condition"]
    F --> H["Negative → unless modifier\nputs x unless condition"]
    D --> I{One line\nor multi-line?}
    I --> J["One line → ternary\ncondition ? a : b"]
    I --> K["Multi-line → if-else"]
    E --> L{Match a value,\nclass, or range?}
    L --> M["Yes → case/when"]
    L --> N["No → if-elsif-else"]
    E --> O["Guard clauses at the start\nof a method → early return"]
Quick guide to choosing a construct:
  if postfix modifier      → one branch, simple condition, one line
  unless postfix modifier  → one branch, negative condition, one line
  if-else                  → two branches, expression or multi-line
  unless-else              → two branches, when the negative condition reads better
  ternary ? :              → two branches, one line, returns a value
  if-elsif-else            → many branches, arbitrary conditions
  case/when by value       → many branches, match value, class, range, regex
  Guard clause + return    → early validation before the main logic

Summary #

  • if, unless, and case are expressions — they all return values and can be used directly in assignments, not just as statements.
  • The order of elsif determines the result — put the most specific (strictest) condition on top to avoid a broader condition “swallowing” cases that should fall through to lower branches.
  • unless for naturally negative conditions — but avoid unless with ||, &&, or negation inside it because that’s confusing.
  • Postfix modifiers for one-liners without elseputs x if condition and return unless valid? are very common Ruby idioms that read naturally.
  • Guard clauses + early return reduce nesting — handle all edge cases at the start of a method with return unless, keeping the main logic un-nested.
  • case/when uses === — not ==, so it can match by class (Integer), range (1..10), regex (/\d+/), and values all at once.
  • case without an expression — can serve as a cleaner if-elsif replacement for conditions that don’t fit a single variable.
  • case/in pattern matching — a Ruby 3.x feature that allows extracting (destructuring) data from Hashes and Arrays directly in when.
  • Ternary only for one-line two-branch cases — don’t nest ternaries, don’t use them for side effects without a return value.

← Previous: Operators   Next: Loops →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact