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 useunlesswith conditions containing negation or the||and&&operators — the result is hard to read and easy to misread. Simple rule: if you needunlesswith||or&&, useifwith 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, andcaseare expressions — they all return values and can be used directly in assignments, not just as statements.- The order of
elsifdetermines the result — put the most specific (strictest) condition on top to avoid a broader condition “swallowing” cases that should fall through to lower branches.unlessfor naturally negative conditions — but avoidunlesswith||,&&, or negation inside it because that’s confusing.- Postfix modifiers for one-liners without
else—puts x if conditionandreturn 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/whenuses===— not==, so it can match by class (Integer), range (1..10), regex (/\d+/), and values all at once.casewithout an expression — can serve as a cleanerif-elsifreplacement for conditions that don’t fit a single variable.case/inpattern matching — a Ruby 3.x feature that allows extracting (destructuring) data from Hashes and Arrays directly inwhen.- Ternary only for one-line two-branch cases — don’t nest ternaries, don’t use them for side effects without a return value.