Statement Modifiers
A statement modifier is Ruby’s way of attaching a condition or a loop to a single statement without writing a full if...end or while...end block. Instead of wrapping one line in four lines of block syntax, you write the condition after the statement itself: puts "hi" if happy. Ruby embraces this style because it favors reading code the way you’d say it out loud, and it’s one of the most recognizable habits of idiomatic Ruby.
Overview / How it works
Five keywords can act as statement modifiers: if, unless, while, until, and rescue. In every case, Ruby’s parser recognizes the trailing keyword and treats the line as equivalent to the corresponding block form — it is not a different feature, just a more compact grammar for the same underlying behavior. This is possible because in Ruby almost everything is an expression with a value, including if and case themselves, so a single line can carry both an action and the logic that governs whether it runs.
if and unless modifiers
statement if condition evaluates condition first. If it is truthy, the statement runs; otherwise it is skipped entirely. Remember Ruby’s truthiness rule: only nil and false are falsy — 0, "", and [] are all truthy. unless is simply the negation: the statement runs only when the condition is falsy. Neither modifier form supports elsif or else; if you need branching beyond a single yes/no guard, you need the full block form.
while and until modifiers
statement while condition and statement until condition behave like ordinary pretest loops: Ruby checks the condition, and only if it passes (truthy for while, falsy for until) does it run the statement, then checks again, and so on. That means the statement can run zero times if the condition already fails on the first check.
The begin…end while special case
There is one important exception. If you wrap the body in begin...end and attach while or until to the end, Ruby switches to a posttest loop: the body runs once first, and only then is the condition checked before deciding whether to repeat. This is the only way to get a guaranteed-at-least-once loop using these modifiers — the equivalent of a “do…while” loop in languages like C or Java. It is easy to reach for this syntax out of habit and forget that it changes the timing of the condition check, which is a common source of bugs (covered below).
The rescue modifier
expression rescue fallback evaluates expression. If it raises any exception that is a subclass of StandardError (which covers the vast majority of everyday exceptions like ZeroDivisionError, NoMethodError, and ArgumentError, but deliberately excludes rarer, more dangerous exceptions like SyntaxError or NoMemoryError), Ruby evaluates fallback instead and that becomes the value of the whole line. If no exception is raised, the value of expression is used and fallback is never touched.
Syntax
statement if condition
statement unless condition
statement while condition
statement until condition
begin
statement
end while condition
statement rescue fallback_expression
| Modifier | Runs the statement when… | Loop timing |
|---|---|---|
if |
condition is truthy | n/a |
unless |
condition is falsy (nil or false) |
n/a |
while |
condition is truthy, checked before each run | pretest — zero or more times |
until |
condition is falsy, checked before each run | pretest — zero or more times |
begin...end while/until |
condition checked after the first run | posttest — one or more times |
rescue |
the left side raises a StandardError |
n/a |
Examples
Example 1: the if modifier as a guard clause
def check_voting_age(age)
puts "You can vote." if age >= 18
puts "Checked age #{age}."
end
check_voting_age(20)
check_voting_age(16)
You can vote.
Checked age 20.
Checked age 16.
For check_voting_age(20), age >= 18 is true, so the first puts runs, then the second always runs. For check_voting_age(16), the condition is false, so only the second line prints. This is the classic Ruby guard-clause idiom: put the condition after the single line it controls instead of wrapping it in a block.
Example 2: unless plus pretest while/until loops
def describe_temperature(temp)
puts "Freezing!" if temp <= 0
puts "Not freezing." unless temp <= 0
end
describe_temperature(-5)
describe_temperature(15)
count = 0
count += 1 while count < 5
puts "Count is #{count}"
countdown = 5
countdown -= 1 until countdown.zero?
puts "Countdown is #{countdown}"
Freezing!
Not freezing.
Count is 5
Countdown is 0
With temp = -5, temp <= 0 is true, so only “Freezing!” prints; the unless line is skipped because its condition (the negation) is false. With temp = 15, it’s the reverse. The while loop increments count from 0 up to 5, checking count < 5 before each step, stopping the moment it becomes false. The until loop decrements countdown from 5 down to 0, running as long as countdown.zero? is false.
Example 3: the rescue modifier for lightweight error handling
def safe_divide(a, b)
result = a / b rescue nil
result
end
puts safe_divide(10, 2)
puts safe_divide(10, 0).inspect
def parse_number(str)
Integer(str) rescue nil
end
puts parse_number("42").inspect
puts parse_number("abc").inspect
5
nil
42
nil
safe_divide(10, 2) divides cleanly to 5, so rescue nil never triggers. safe_divide(10, 0) raises ZeroDivisionError, which is a StandardError, so the modifier catches it and result becomes nil. The same pattern turns Integer("abc")‘s ArgumentError into a clean nil instead of crashing the program, which is a common idiom for lenient parsing.
How it works step by step
For puts "ok" if ready: Ruby first evaluates ready. If it is truthy, it then evaluates the left-hand statement and runs it; if falsy, the left-hand statement is never executed at all — not even partially. Note, however, that the statement was still parsed when the file loaded, so any local variable it assigns is declared (see Common Mistakes below).
For a pretest while/until modifier, Ruby repeats: check condition, and if it passes, run the statement, then check the condition again. The very first check can prevent the statement from running at all. For the begin...end while form, Ruby instead does: run the statement once, then check the condition, and repeat only if it still passes — the check happens strictly after the first execution.
For expression rescue fallback, Ruby wraps the entire evaluation of expression — including any method calls or blocks nested inside it — in an implicit exception handler. The moment any code within that expression raises a StandardError (or subclass), evaluation of expression is abandoned and fallback is evaluated in its place.
Common Mistakes
Mistake 1: a variable inside a skipped modifier still exists, as nil
message = "hi" if false
puts message.inspect
nil
Because Ruby’s parser sees the assignment message = "hi" at parse time, it declares message as a local variable no matter whether the if condition ever lets the assignment actually execute. The result is nil, not a NameError, which can silently hide a bug where you expected the variable to be undefined or to raise. Fix it by initializing the variable explicitly so its default value is obvious:
message = nil
message = "hi" if true
puts message
hi
Mistake 2: begin…end while runs the body once even if the condition starts false
x = 10
begin
puts "Processing #{x}"
x += 1
end while x < 5
Processing 10
It’s tempting to assume this is just a more compact while loop, but begin...end while is a posttest loop: the body always runs at least once, even though x < 5 is false from the start. If you actually want the pretest behavior (skip the body entirely when the condition already fails), use a plain while block instead:
x = 10
while x < 5
puts "Processing #{x}"
x += 1
end
puts "Done"
Done
Mistake 3: rescue modifiers silently swallow unrelated bugs
def total_price(items)
items.sum { |item| item.pric } rescue 0
end
puts total_price([{price: 10}])
0
The typo item.pric (missing the closing letter of price) raises NoMethodError, which is a StandardError — so the bare rescue 0 catches it just as happily as it would catch a genuine, expected error, and silently returns 0 instead of the real total. The bug hides behind a plausible-looking number. Prefer to fix the actual bug and only reach for rescue where failure is truly expected and safe to ignore:
def total_price(items)
items.sum { |item| item[:price] }
end
puts total_price([{price: 10}, {price: 25}])
35
Best Practices
- Reserve statement modifiers for short, single-line actions and simple guard clauses; if the condition or statement is long or nested, use the full block form for readability.
- Remember modifiers don’t support
elsiforelse— reach for a fullif/elsif/else/endas soon as you need more than one branch. - Prefer
ifwith a positive condition overunlesswith a compound or negated one;unless x.nil? || yreads far worse than the equivalent positiveif. - Use
begin...end while/untildeliberately and rarely — it is easy to reach for out of habit and get an unwanted extra execution. - Avoid bare
rescue(or a barerescuemodifier) as a blanket error suppressor; catch specific exception classes with a fullbegin/rescueblock when you need to handle more than one predictable failure case. - Initialize variables you plan to reference outside a modifier’s scope before the modifier line, so their default value is intentional rather than an accidental
nil.
Practice Exercises
1. Write a one-line method that takes a number and prints "Even!" using an unless modifier, only when the number is not even.
2. Rewrite the following block using a single-line while statement modifier: total = 0; while total < 20; total += 4; end. What value does total end up with?
3. Write a method average(numbers) that divides the sum of an array by its length, using a rescue modifier to return 0 if the array is empty (which would otherwise raise ZeroDivisionError). What does average([]) print, and what does average([2, 4, 6]) print?
Summary
- Statement modifiers (
if,unless,while,until,rescue) let you attach a condition, loop, or error handler to a single statement without a full block. if/unlessmodifiers run the statement based on truthiness — onlynilandfalseare falsy in Ruby.while/untilmodifiers are pretest loops that may run zero times; wrapping the body inbegin...endswitches to a posttest loop that always runs at least once.rescueas a modifier catchesStandardErrorand its subclasses, evaluating a fallback expression in their place — use it sparingly so it doesn’t hide real bugs.- A variable assigned inside a modifier that never executes is still declared, with a value of
nil, because Ruby declares local variables at parse time. - Modifiers cannot express
elsif/elsebranching; switch to the full block form once logic grows beyond a single condition.
