Ruby keyword: until
until expr [do] ... end What the until keyword does
until is Ruby’s inverse of while. The body runs while the condition is false and exits the first time the condition becomes true. That is the entire mental model. Everything else (return value, scope, parser quirks) is the same as while, so if you already know while you mostly need to know when until reads better than while.
The official docs put it directly: “The until loop executes while a condition is false.” Same parser, same do-optional rule, same break-supplies-return-value rule, same modifier form, same begin/end do-once form.
The three forms
Block form
until expr [do]
body
end
expr is any Ruby expression. Falsy means “keep looping.” The do keyword is optional; drop it when the body starts on a new line.
a = 0
until a > 3
puts a
a += 1
end
# 0
# 1
# 2
# 3
The loop prints 0 through 3 and stops when a reaches 4. The condition is checked before each iteration, including the first, so if a > 3 is already true on entry the body never runs.
Modifier form
body until expr
This is a statement, not an expression. The modifier form is most useful for short, side-effecting loops where pulling the body out into its own multi-line block would obscure the intent. Use it for one-line polls and accumulators:
a = 0
a += 1 until a > 3
a
# => 4
The modifier form cannot appear directly as a method argument without extra parentheses, because the parser keeps reading past ( and treats until as part of the argument list. Once it reaches the next until, the parser raises a SyntaxError, since the modifier form is a statement rather than an expression:
puts(1 until false)
# SyntaxError: unexpected keyword_until
Wrap it or add a space after the method name. Both forms work, but doubled parentheses make the intent explicit and survive a round of automatic formatting, whereas the space trick is easy to break with a careless reformat:
puts((1 until false)) # OK; prints nil
puts (1 until false) # OK
Begin/end form
Sometimes you need the body to run at least once regardless of whether the condition is true at entry. The third form of until does exactly that, mirroring C’s do/while:
begin
body
end until expr
The body runs at least once. The condition is checked after the body, just like C’s do/while. Reach for this variant when the loop is a state-mutating retry, such as incrementing an attempt counter, retrying a connection, or draining a queue until it is empty:
attempt = 0
begin
attempt += 1
end until attempt >= 3
attempt
# => 3
If attempt had started at 5, the body would still run once (making it 6) before the now-true condition exits the loop.
Truthy and falsy
until uses the same truth rules as if, while, and unless. Only nil and false are falsy. 0, "", [], and {} are all truthy:
arr = []
until arr # `arr` is truthy (it is an Array, even an empty one)
# never runs
end
until arr.empty? # the test most readers actually want
# ...
end
A surprising number of beginners write until some_array expecting it to loop until the array is empty. It does not, because an empty array is still truthy.
Return value
The whole until expression returns nil unless you break out with a value. A normal exit through the condition flipping to truthy returns nil. Only break value (or an uncaught exception) can produce a different return:
status = until (input = gets).nil?
break :eof if input.chomp == "quit"
end
status
# => :eof if you typed "quit"
# => nil if you hit Ctrl-D / EOF first
next and redo keep the return value at nil because they do not exit the loop.
Scope leaks
Like while and for, variables assigned inside an until body survive after the loop ends:
until ready?
attempt ||= 0
attempt += 1
end
attempt # the local is still in scope after the loop
This is the opposite of each, times, and other block-based iteration, where block-scoped locals vanish. It is occasionally useful (capturing terminal state without a find), occasionally surprising. If you want block scoping, use 1.times { ... } or one of the enumerator methods.
Break, next, and redo
Same behavior as inside while or each:
breakexits the loop and can supply a return value.nextskips the rest of the body and jumps back to the condition check.redore-runs the body without re-testing the condition. If the body does not mutate state,redowill hang.
i = 0
until i >= 5
i += 1
next if i.even?
puts i
end
# 1
# 3
# 5
Common mistakes
until is not unless
This is the single most common confusion. unless is a one-shot conditional (the inverse of if); until is a loop (the inverse of while). They look similar and both use the “negated positive” reading, but they belong to different grammar categories:
# unless: runs once if the condition is false
puts "hi" unless seen?
# until: loops while the condition is false
until seen?
# ...
end
Operator precedence in the test
until a += 1 > 10 parses as until a += (1 > 10) because > binds tighter than +=. Almost always a bug. Parenthesize:
until (a += 1) > 10
# ...
end
Parser order with first-iteration variables
The body of a modifier-form until runs after the condition is parsed, but locals are still resolved at parse time. This means a modifier loop that introduces its own loop variable on the first iteration will not work, even though it looks identical to a while loop in many languages:
p a until a = 0.zero?
# NameError: undefined local variable or method `a`
The parser sees a on the left of = before it sees the assignment, treats it as a read, and blows up. Fix: seed the variable first.
a = 0.zero?
p a until a
# prints false, then exits
Negative conditions
until !cond and while cond are the same loop. Pick one and stay consistent. Most Ruby code reads more naturally with positive conditions, so while cond tends to win, but until connected? reads better than while !connected?.
When to reach for until
Use it when the positive form is awkward or when you are polling:
sleep 0.1 until File.exist?("/tmp/ready.flag")
until user.confirmed?
render_confirmation
prompt_again
end
For iterating collections, prefer each, map, times, upto, downto, or each_with_index. For an infinite loop, loop { break if ... } reads better than until true ... end.