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