Overview
Ruby implements two primary conditional loop constructs: while loops that continue execution as long as a condition remains true, and until loops that continue until a condition becomes true. Both loops evaluate their conditions before each iteration and support modifier syntax for single-line expressions.
The while loop executes its block repeatedly while the given condition evaluates to a truthy value. The until loop provides the logical inverse, executing while the condition evaluates to a falsy value. Ruby treats all values as truthy except false and nil.
counter = 0
while counter < 3
puts "Iteration #{counter}"
counter += 1
end
# => "Iteration 0"
# => "Iteration 1"
# => "Iteration 2"
counter = 0
until counter == 3
puts "Until iteration #{counter}"
counter += 1
end
# => "Until iteration 0"
# => "Until iteration 1"
# => "Until iteration 2"
Both constructs return nil and support the modifier syntax for concise single-line operations:
counter = 0
counter += 1 while counter < 5
puts counter # => 5
numbers = [1, 2, 3, 4, 5]
numbers.pop until numbers.length == 2
puts numbers # => [1, 2]
Basic Usage
The standard while loop syntax requires a condition expression followed by a block terminated with end. The condition is evaluated before each iteration, making it a pre-test loop structure.
input = ""
while input != "quit"
print "Enter command (or 'quit'): "
input = gets.chomp.downcase
case input
when "hello"
puts "Hello there!"
when "time"
puts Time.now
when "quit"
puts "Goodbye!"
else
puts "Unknown command: #{input}"
end
end
The until loop provides more readable code when the terminating condition is more natural to express as a positive assertion:
password_attempts = 0
valid_password = false
until valid_password || password_attempts >= 3
print "Enter password: "
input_password = gets.chomp
if input_password == "secret123"
valid_password = true
puts "Access granted!"
else
password_attempts += 1
puts "Invalid password. #{3 - password_attempts} attempts remaining."
end
end
puts "Access denied!" unless valid_password
Modifier syntax transforms simple operations into concise one-liners. The expression executes once before the condition is evaluated, making modifier loops post-test constructs:
# Reading file lines until empty
file = File.open("data.txt")
begin
line = file.gets
process_line(line) if line
end while line
file.close
# Building arrays with conditions
result = []
num = 1
result << num and num += 1 until result.length == 10
puts result # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Nested loops handle multi-dimensional iteration scenarios:
row = 0
while row < 3
col = 0
while col < 3
print "[#{row},#{col}] "
col += 1
end
puts
row += 1
end
# => [0,0] [0,1] [0,2]
# => [1,0] [1,1] [1,2]
# => [2,0] [2,1] [2,2]
Advanced Usage
Complex loop control requires understanding Ruby's break, next, and redo statements. The break statement terminates the loop immediately, while next skips to the next iteration. The redo statement repeats the current iteration without re-evaluating the condition.
numbers = []
counter = 0
while counter < 100
counter += 1
# Skip even numbers
next if counter.even?
# Stop at first number divisible by 13
break if counter % 13 == 0
# Redo if we hit a multiple of 7 (creates infinite loop protection needed)
if counter % 7 == 0 && counter < 50
puts "Skipping #{counter} (multiple of 7)"
next
end
numbers << counter
end
puts "Collected numbers: #{numbers}"
puts "Stopped at: #{counter}"
Loop variables maintain scope outside the loop construct, unlike block iterators. This behavior enables accumulator patterns and state preservation:
fibonacci_sequence = [1, 1]
current_sum = 2
while fibonacci_sequence.length < 20
next_number = fibonacci_sequence[-1] + fibonacci_sequence[-2]
fibonacci_sequence << next_number
current_sum += next_number
# Early termination if sum exceeds threshold
break if current_sum > 10000
end
puts "Generated #{fibonacci_sequence.length} Fibonacci numbers"
puts "Sum: #{current_sum}"
puts "Sequence: #{fibonacci_sequence.join(', ')}"
Exception handling within loops requires careful consideration of control flow. Exceptions can propagate out of loops or be handled internally:
retry_count = 0
max_retries = 3
success = false
until success || retry_count >= max_retries
begin
# Simulate unreliable operation
result = perform_network_request()
if result.status == 200
success = true
puts "Operation succeeded on attempt #{retry_count + 1}"
else
raise StandardError, "HTTP #{result.status}"
end
rescue StandardError => e
retry_count += 1
puts "Attempt #{retry_count} failed: #{e.message}"
# Exponential backoff
sleep(2 ** retry_count) if retry_count < max_retries
end
end
puts success ? "Final success!" : "Failed after #{max_retries} attempts"
Proc and lambda objects can encapsulate complex loop termination logic:
# Complex condition as a proc
termination_condition = proc do |data, iterations|
data.length > 100 ||
iterations > 50 ||
data.last&.negative?
end
data_points = [1]
iteration_count = 0
until termination_condition.call(data_points, iteration_count)
new_value = data_points.last * rand(0.8..1.2) + rand(-5..5)
data_points << new_value.round(2)
iteration_count += 1
puts "Iteration #{iteration_count}: #{new_value}" if iteration_count % 10 == 0
end
puts "Terminated after #{iteration_count} iterations"
puts "Final data size: #{data_points.length}"
Common Pitfalls
Infinite loops represent the most frequent pitfall in conditional loops. The condition must eventually change within the loop body, or external intervention must terminate execution:
# DANGEROUS: Infinite loop
counter = 0
while counter < 10
puts "This will run forever"
# counter never increments!
end
# SAFE: Proper counter management
counter = 0
while counter < 10
puts "Iteration #{counter}"
counter += 1 # Essential increment
end
Variable scoping creates subtle bugs when loop variables shadow outer scope variables:
counter = 100 # Outer scope variable
# This shadows the outer counter
counter = 0
while counter < 5
puts "Inner counter: #{counter}"
counter += 1
end
puts "Outer counter: #{counter}" # => 5, not 100!
# Better approach: Use different variable names
outer_counter = 100
loop_index = 0
while loop_index < 5
puts "Loop index: #{loop_index}, Outer: #{outer_counter}"
loop_index += 1
end
puts "Preserved outer counter: #{outer_counter}" # => 100
Modifier syntax creates post-test loops, executing the statement at least once regardless of the condition:
# Dangerous with modifier syntax
balance = 0
balance -= 100 while balance > 0
puts balance # => -100 (executed once even though condition was false)
# Safer with block syntax
balance = 0
while balance > 0
balance -= 100
end
puts balance # => 0 (never executed)
Floating-point arithmetic introduces precision errors that can break termination conditions:
# Problematic floating-point loop
value = 0.1
while value != 1.0
puts value
value += 0.1
break if value > 2.0 # Safety break to prevent infinite loop
end
puts "Final value: #{value}" # Never equals exactly 1.0
# Better approach with tolerance
value = 0.1
target = 1.0
tolerance = 0.0001
while (value - target).abs > tolerance
puts value.round(2)
value += 0.1
break if value > 2.0 # Safety break
end
puts "Final value: #{value.round(2)}"
Ruby's truthiness can create unexpected behavior when working with return values:
# Unexpected behavior with truthiness
lines = []
file = File.open("data.txt")
# This stops when gets returns an empty string, not just nil
while line = file.gets
lines << line.chomp unless line.empty? # Empty lines are truthy!
end
file.close
# Explicit nil check is clearer
lines = []
file = File.open("data.txt")
while (line = file.gets) != nil
lines << line.chomp
end
file.close
Exception handling can disrupt expected loop flow:
# Problematic exception handling
counter = 0
while counter < 10
begin
risky_operation(counter)
rescue StandardError => e
puts "Error at #{counter}: #{e.message}"
next # This skips the counter increment!
end
counter += 1 # Never reached after exceptions
end
# Proper exception handling
counter = 0
while counter < 10
begin
risky_operation(counter)
rescue StandardError => e
puts "Error at #{counter}: #{e.message}"
ensure
counter += 1 # Always increments
end
end
Reference
While Loop Syntax
| Syntax | Description | Evaluation |
|---|---|---|
while condition; statements; end |
Block form with semicolon | Pre-test |
while condition\n statements\n end |
Block form with newlines | Pre-test |
statement while condition |
Modifier form | Post-test |
begin; statements; end while condition |
Begin-while form | Post-test |
Until Loop Syntax
| Syntax | Description | Evaluation |
|---|---|---|
until condition; statements; end |
Block form with semicolon | Pre-test |
until condition\n statements\n end |
Block form with newlines | Pre-test |
statement until condition |
Modifier form | Post-test |
begin; statements; end until condition |
Begin-until form | Post-test |
Control Flow Statements
| Statement | Effect | Scope |
|---|---|---|
break |
Terminate loop immediately | Current loop only |
break value |
Terminate loop and return value | Current loop only |
next |
Skip to next iteration | Current loop only |
next value |
Skip iteration with value | Current loop only |
redo |
Repeat current iteration | Current loop only |
Truthiness Rules
| Value | Truthiness | Loop Behavior |
|---|---|---|
true |
Truthy | while continues, until stops |
false |
Falsy | while stops, until continues |
nil |
Falsy | while stops, until continues |
0 |
Truthy | while continues, until stops |
"" (empty string) |
Truthy | while continues, until stops |
[] (empty array) |
Truthy | while continues, until stops |
{} (empty hash) |
Truthy | while continues, until stops |
Return Values
| Construct | Return Value | Notes |
|---|---|---|
while condition; end |
nil |
Always returns nil |
until condition; end |
nil |
Always returns nil |
break |
nil |
Default break value |
break value |
value |
Returns specified value |
| Modifier loops | Result of last expression | Post-test evaluation |
Common Patterns
# Counter loop
i = 0
while i < limit
# operations
i += 1
end
# Accumulator pattern
sum = 0
i = 1
while i <= n
sum += i
i += 1
end
# Input processing
input = nil
while input != "quit"
input = gets.chomp
# process input
end
# Conditional accumulation
results = []
current = start_value
while condition(current)
results << current if meets_criteria(current)
current = next_value(current)
end
# File processing
File.open(filename) do |file|
while line = file.gets
process_line(line)
end
end
# Retry with exponential backoff
attempts = 0
max_attempts = 5
begin
# risky operation
rescue SomeError
attempts += 1
retry if attempts < max_attempts
sleep(2 ** attempts)
end