rubyguides

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:

  • break exits the loop and can supply a return value.
  • next skips the rest of the body and jumps back to the condition check.
  • redo re-runs the body without re-testing the condition. If the body does not mutate state, redo will 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.

See also