Control Expressions
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18). Ruby has no separate "statement" world for control
flow: every control expression described here returns a value, so if, unless, case, while and begin can be
assigned, returned and passed around like any other expression. This page covers the whole family: how Ruby decides
whether a condition is true, if/elsif/else/unless and their modifier forms, the ternary operator, combining
conditions, case/when and the === operator behind it, the loops (while, until, for, loop) and why the
iterator methods are usually preferred, break/next/redo and the values they carry, catch/throw for expected
non-local exits, the flip-flop, and BEGIN/END. Features newer than Ruby 3.0 name their version.
It builds on Operators and Precedence (the precedence of
&&, ||, and, or, not and the ternary operator, <=>, ===) and
Assignment and Multiple Assignment (assignment
in a condition). Pages that continue from here are Pattern Matching
(case/in), Blocks, Procs and Lambdas (what return,
next and break do in a block, a proc and a lambda),
Exceptions and Error Handling (rescue, ensure,
retry), Enumerable and
Enumerators and Lazy Evaluation (the alternatives
to explicit loops). For how other languages express the same ideas, see the control-flow pages of
Rust and Go in this section of the
site, noting the main difference: in Ruby the branching constructs are expressions, there is no switch fall-through and
no continue (it is next).
Every example was checked with ruby -wc, run with Ruby 4.0.7 with warnings enabled and linted with RuboCop 1.92 using
the same configuration as the running shelf gem (double-quoted strings, TargetRubyVersion: 4.0, new cops enabled).
The output shown is the real output. A few examples keep a deliberate # rubocop:disable comment on the construct they
illustrate (for, BEGIN/END, the flip-flop, begin ... end while, eval), because the point of the example is
that construct. The examples that use the library assume the shelf gem is on the load path (require "shelf"; see the shelf gem source).
Truthiness: what a condition accepts
The documentation of the control expressions states the rule once, and everything else depends on it: nil and false
are the only false-values; true and every other object are true-values. There is no implicit conversion and no
"falsy zero". 0, 0.0, "", [] and {} are all truthy.
# frozen_string_literal: true
[nil, false, true, 0, "", [], {}, 0.0].each do |value|
puts "#{value.inspect.ljust(5)} is #{value ? 'truthy' : 'falsy'}"
end
nil is falsy
false is falsy
true is truthy
0 is truthy
"" is truthy
[] is truthy
{} is truthy
0.0 is truthy
Source: Control Expressions
Three consequences are worth knowing:
-
a condition is never required to be a boolean, so methods such as
Array#find,Hash#[],String#=~andRegexp#matchcan be used directly as conditions: they return an object (truthy) ornil(falsy); -
a predicate method (name ending in
?) conventionally returnstrueorfalse, but the language does not enforce it; many core predicates return truthy values ornil, for exampleInteger#nonzero?returns the receiver ornil. Write code that depends only on truthiness, not on getting exactlytrue; -
!xturns any object into a boolean (the negation), and!!xinto its truthiness as a boolean. The double negation is occasionally useful at an API boundary (RuboCop’sStyle/DoubleNegationflags it elsewhere), but it is not needed in a condition. The operators themselves are covered in Operators and Precedence.
nil can also be tested with nil?, with &. (safe navigation, which short-circuits a chain on a nil receiver) or by
pattern matching. Which of them reads best depends on what the code does when the value is missing.
if, elsif and else
The simplest form has a test expression and a body; then is optional and omitted by almost every program, except to keep
a short body on the same line (if x then y end, and then after every when). elsif adds any number of further
tests, and else is the branch taken when none matched. Once a branch is taken the rest are not evaluated.
# frozen_string_literal: true
require "shelf"
def shelf_status(copies)
if copies.zero?
"out of stock"
elsif copies < 3
"running low"
else
"plenty"
end
end
puts shelf_status(0)
puts shelf_status(2)
puts shelf_status(10)
# if is an expression: its value is the last value evaluated
lendable = Shelf::Loan::STATES.include?(:lent)
label = lendable ? "lendable" : "static"
puts label
# a branch that is not taken and has no else yields nil
copies = 0
nothing = ("in stock" if copies.positive?)
p nothing
# unless has no elsif; keep an else for the rare case it reads better
states = Shelf::Loan::STATES
if states.empty?
puts "no states"
else
puts "#{states.size} states known"
end
out of stock
running low
plenty
lendable
nil
4 states known
The rules the example shows, and the guide states:
-
the value of an
ifis the last value evaluated in the branch taken. When no branch is taken (a bareifwith a false condition, noelse), the value isnil; -
because
ifis an expression, you can assign it, return it or pass it as an argument. Prefer the assignment-from-ifform to assigning the same variable in every branch; -
the keyword is
elsif: notelseif,else if(which opens a nestedifthat needs its ownend) orelif; -
unlessis the negation ofif. It acceptselsebut notelsif: writingelsifafterunlessis a syntax error; -
the body of a modifier or
thenform can be a single expression; for anything longer, use the multi-line form.
The condition may assign
The tests may have side effects, and the idiomatic use is to cache a value in a local variable and test it in one step. Wrap the assignment in parentheses to show it is deliberate.
# frozen_string_literal: true
require "shelf"
catalog = Shelf::Catalog.new
catalog.add(Shelf::Book.new(isbn: Shelf::Isbn.new(digits: "978-0-13-468599-1"), title: "The Pragmatic Programmer",
author: Shelf::Author.new(name: "Thomas")))
# the parentheses document that the assignment is intended
if (book = catalog.find { it.title.start_with?("The") })
puts "found #{book}"
end
found The Pragmatic Programmer by Thomas
The variable is visible after the if too: a block of if, unless, case, while and for does not create a new
local-variable scope (only def, class, module and blocks do; see
Variables, Constants and Scope).
A mistyped = for == is the classic bug here. Ruby warns about the clearest cases when warnings are enabled with
ruby -w (or $VERBOSE = true): assigning a literal in a condition, and using a literal as a condition at all. These are the
real messages from running the checks with Ruby 4.0.7:
w2.rb:4: warning: found '= literal' in conditional, should be ==
w2.rb:7: warning: found '= literal' in conditional, should be ==
w2.rb:10: warning: string literal in condition
Source: Warning
The warning appears for if copies = 1, if copies = "text" and puts "ok" if "always". It does not appear for
if (match = regexp.match(text)), where the right-hand side is not a literal. RuboCop’s Lint/LiteralAsCondition and
Lint/LiteralAssignmentInCondition check the same things statically.
Conditions can also be patterns
Since Ruby 3.1 (experimental in 3.0), the one-line in and => operators are expressions that can stand in a condition: value in pattern
returns true or false, and value => pattern raises NoMatchingPatternError when the pattern does not match. The
case/in form is the full pattern-matching construct and has its own page,
Pattern Matching; the examples below show how the three forms
relate to ordinary conditions. Variables bound by a pattern stay in scope after the construct, as with an assigning
if.
# frozen_string_literal: true
config = { env: "production", debug: false, workers: 4 }
if config in { env: "production", debug: false }
puts "locked down"
end
if config in { workers: Integer => n }
puts "#{n} workers" if n > 2
end
config => { env: }
puts env
case config
in { env: "development" } then puts "dev"
in { env: String => name, workers: } if workers > 3 then puts "#{name} with many workers"
end
number = 5
begin
case number
in String then 1
end
rescue NoMatchingPatternError => e
puts "#{e.class}: #{e.message}"
end
locked down
4 workers
production
production with many workers
NoMatchingPatternError: 5: String === 5 does not return true
Source: Pattern Matching
The failure of the last case shows the one difference from case/when: a case/in without a matching branch and
without else raises NoMatchingPatternError, while case/when returns nil.
unless
unless runs its body when the condition is false. It exists to make a negative condition read like English; the style
guides agree that it is the right keyword for a short, single negative condition and the wrong one as soon as the
condition becomes compound or has an else.
-
unless xisif !x(orif not x), nothing more; -
never use
elsifwith it (syntax error), and avoidelse: RuboCop’sStyle/UnlessElsesuggests swapping the branches and usingif; -
avoid
unless a && !band similar: a reader has to negate the whole condition in their head. Name the condition with a variable or a predicate method instead (see Combining conditions); -
unlessis excellent as a modifier in a guard clause (return unless valid?,raise ArgumentError, "..." unless ok).
The error you get for elsif after unless is a syntax error reported when the file is parsed, so nothing in the file
runs:
w7.rb:5: warning: possibly useless use of false in void context
w7.rb: --> w7.rb
unexpected 'elsif', ignoring it
> 3 unless true
> 5 elsif false
> 7 end
w7.rb:5: syntax error found (SyntaxError)
3 | unless true
4 | puts 1
> 5 | elsif false
| ^~~~~ unexpected 'elsif', ignoring it
6 | puts 2
7 | end
Since Ruby 3.4 the default parser is Prism and the diagnostics have this multi-line, caret-annotated form; the same mistakes were a
single syntax error, unexpected ... line in Ruby 3.3 and earlier.
Modifier if and unless, and guard clauses
Both keywords can follow an expression: body if test and body unless test. The left side is the "then" statement, the
right side the test, and the test is evaluated first.
# frozen_string_literal: true
def checkout(isbn)
return "no ISBN" if isbn.nil?
return "blank ISBN" if isbn.empty?
"checking out #{isbn}"
end
puts checkout(nil)
puts checkout("")
puts checkout("9780134092669")
counter = 0
counter += 1 while counter < 10
p counter
# begin ... end while runs the body once before testing
runs = 0
begin
runs += 1
end while runs.negative? # rubocop:disable Lint/Loop
p runs
no ISBN
blank ISBN
checking out 9780134092669
10
1
The first method is a guard clause: validate and return early at the top of a method, so that the main path is not nested
inside a pyramid of if/else. Good guard clauses are short, use the modifier form, and report errors with the most
specific exception class. Raising on invalid arguments is the usual shape for library code:
# frozen_string_literal: true
require "shelf"
def lend(book, days)
raise ArgumentError, "book required" if book.nil?
raise ArgumentError, "days must be positive: #{days}" unless days.positive?
Shelf::Loan.new(book, state: :lent)
end
book = Shelf::Book.new(isbn: Shelf::Isbn.new(digits: "978-0-13-468599-1"), title: "Pragmatic",
author: Shelf::Author.new(name: "Thomas"))
puts lend(book, 14).describe
begin
lend(book, 0)
rescue ArgumentError => e
puts "#{e.class}: #{e.message}"
end
Pragmatic is lent
ArgumentError: days must be positive: 0
A few cautions about modifiers:
-
a long condition at the end of a line hides the branch; keep modifier lines under the usual length and move compound tests into a named variable or predicate method;
-
do not chain modifiers (
x if a unless b): they parse as(x if a) unless b, which no one reads correctly; -
the modifier form is a statement, not an expression (see below), which matters when you pass it as an argument.
The parse-order trap
A modifier and the standard form are not exact transformations of each other. Ruby parses the file from left to right to decide which names are local variables, but runs the test of a modifier first. So a variable assigned in the test is not yet known as a variable in the body to its left:
# frozen_string_literal: true
source = "p value if value = 0.zero?"
begin
eval(source, TOPLEVEL_BINDING, "modifier.rb", 1) # rubocop:disable Security/Eval
rescue NameError => e
puts "#{e.class}: #{e.message}"
end
NameError: undefined local variable or method 'value' for main
When the parser reaches p value, value has not yet been assigned in the source, so it is compiled as a method call.
The assignment on the right is then executed first, but it does not change the earlier decision. The eval is only
there to turn the file’s NameError into something the example can show; in a normal file, the same line fails the same
way. The message format is the Ruby 3.4 one ('value' with straight quotes, no backtick, and for main where Ruby 3.2
and earlier wrote for main:Object). The cure is to assign before the use, or to use the normal if/end form.
The ternary operator
condition ? a : b is an if/else that fits in an expression. Use it when both branches are short and simple; use
a full if or a case when they are not. The documentation recommends it only for simple conditionals and warns against
several ternaries in one expression.
# frozen_string_literal: true
def plural(count, word) = "#{count} #{count == 1 ? word : "#{word}s"}"
puts plural(1, "book")
puts plural(3, "book")
# prefer case over a nested ternary
n = 15
size =
case n
when ...10 then "small"
when 10...100 then "medium"
else "large"
end
puts size
1 book
3 books
medium
Source: Control Expressions: Ternary if
Points to remember:
-
write whitespace around both
?and:, and give predicate names their?without a space (ready? ? a : b); the symbol and character-literal syntaxes (:a,?a) make cramped ternaries ambiguous; -
the ternary has lower precedence than
||,&&and.., and higher than assignment andnot, soa = b ? c : dassigns the whole ternary, andputs x ? "a" : "b"needs no parentheses; -
never nest ternaries. RuboCop’s
Style/NestedTernaryOperatorflags it. The example shows thecasethat replaces them, with beginless and endless ranges (Ruby 2.7 and 2.6 respectively) inwhen; -
a multi-line ternary is allowed (the
?or:can end a line) but a plainifis clearer.
Combining conditions
Boolean logic has two families of operators, and they differ in precedence and nothing else.
-
&&,||and!bind tightly and are the ones to use inside conditions; -
and,orandnotbind looser than assignment (andand/orhave equal precedence, left to right), so they work as flow control, in the Perl style:command or raise "failed".
# frozen_string_literal: true
def lookup(title) = title == "Dune" ? "found" : nil
# && and || bind tighter than =; 'and' and 'or' bind looser than =
yes = true
no = false
a = no || yes
b = (no or yes)
c = no or yes
p [a, b, c]
# 'or' as flow control: the assignment happens first
result = lookup("Unknown") or puts("not in the catalogue")
p result
lookup("Dune") and puts("Dune is in")
# ! and not differ only in precedence
value = nil
# rubocop:disable Style/Not, Style/RedundantParentheses
p(!value)
p((not value))
# rubocop:enable Style/Not, Style/RedundantParentheses
[true, true, false]
not in the catalogue
nil
Dune is in
true
true
Source: Operator Precedence
The third value in the first line is the trap: c = no or yes assigns no to c and then evaluates yes, discarding
it. That is the reason many style guides avoid and/or altogether. RuboCop’s Style/AndOr (default style
conditionals) allows them only as flow control outside if/while conditions. The result = lookup("Unknown") or puts(...)
line shows the legitimate use: the assignment completes first, and the right operand runs only when the value is falsy.
Both families short-circuit: the right operand is evaluated only when needed, and the whole expression returns the
operand that decided it, not a boolean. nil || "default" returns "default", and user && user.name returns nil
when user is nil. That is why || is the idiom for defaults and && for guarded access (though &. is cleaner for the
latter). not is a keyword that is equivalent to ! except for its precedence; ! is a method that can be redefined,
not calls the same method.
Naming the conditions
When a condition has more than a couple of terms, extract each into a local variable (an explaining variable) or a predicate method (a query method). The condition then reads as the rule it implements. Local variables are best when the terms are used once; predicate methods when they are reused or testable on their own.
# frozen_string_literal: true
require "shelf"
Reader = Data.define(:name, :age, :banned)
def may_borrow?(reader, copies)
adult = reader.age >= 18
in_good_standing = !reader.banned
stock_left = copies.positive?
adult && in_good_standing && stock_left
end
p may_borrow?(Reader.new("Ana", 30, false), 2)
p may_borrow?(Reader.new("Bo", 12, false), 2)
# Ruby 4.0: a line may start with && or || (or and / or) and continues the previous line
def may_borrow_v2?(reader, copies)
reader.age >= 18
&& !reader.banned
&& copies.positive?
end
p may_borrow_v2?(Reader.new("Ana", 30, false), 0)
true
false
false
Source: NEWS for Ruby 4.0.0
The second method uses a feature new in Ruby 4.0: a line that starts with &&, ||, and or or continues the previous
line, like a leading . does for a method chain. Before 4.0 the same code is a syntax error, so write the operator at the end of the line if you must support Ruby 3.4 and earlier. The usual layout rules
apply: align the operators under the first operand, and keep the operators consistent (all leading or all trailing).
case and when
case compares one value against several candidates and runs the first branch that matches. It has two forms.
case with a subject
For each when, Ruby evaluates candidate === subject: the candidate is the receiver and the subject the argument.
This is the case equality operator ===. Object#=== is aliased to ==, and the classes where "matching" means
something more specific override it:
Candidate in when |
=== tests whether the subject … |
Defined in |
|---|---|---|
a class or module ( |
is an instance of it, including subclasses and included modules |
|
a range ( |
falls inside it ( |
|
a regexp ( |
is a string or symbol that matches; sets |
|
a proc or lambda |
makes the callable return a truthy value when called with the subject |
|
a |
is a member (core class in Ruby 4.0) |
|
a symbol, string, number, |
is equal ( |
|
# frozen_string_literal: true
def classify(value)
case value
when nil then "nothing"
when Integer, Float then "number"
when /\A\d{13}\z/ then "bare ISBN-13"
when String, Symbol then "text"
when Array then "list of #{value.size}"
when ->(v) { v.respond_to?(:each) } then "something enumerable"
else "unknown"
end
end
[nil, 3, 2.5, "9780134685991", "Dune", :dune, [1, 2], 1..3, Object].each do |v|
puts "#{v.inspect} -> #{classify(v)}"
end
nil -> nothing
3 -> number
2.5 -> number
"9780134685991" -> bare ISBN-13
"Dune" -> text
:dune -> text
[1, 2] -> list of 2
1..3 -> something enumerable
Object -> unknown
Rules the example demonstrates:
-
branches are tried top to bottom, the first match wins, and later branches are not evaluated. Put the specific tests before the general ones:
/\A\d{13}\z/is placed beforeStringabove because a String also matchesString; -
several candidates on one
whenare separated by commas and tried left to right (a comma means "or"); -
thenlets the body follow on the same line; without it, the body goes on the next line; -
a
casethat has no matching branch and noelsereturnsnil; -
the value of the whole
caseis the value of the branch taken, so it can be assigned or returned directly.
The when list can contain ranges, including beginless and endless ones, which makes numeric classification compact, and
a splat expands an array (or anything with to_a) into candidates:
# frozen_string_literal: true
def loan_fee(days_late)
case days_late
when ..0 then 0
when 1..7 then 25
when 8..30 then 100
when 31.. then 500
end
end
fees = [-3, 0, 1, 7, 8, 30, 31, 400].map { loan_fee(it) }
p fees
# splat a list into when
OPEN_STATES = %i[available lent].freeze
CLOSED_STATES = %i[overdue returned].freeze
def phase(state)
case state
when *OPEN_STATES then :open
when *CLOSED_STATES then :closed
end
end
p phase(:lent), phase(:returned), phase(:lost)
[0, 0, 25, 25, 100, 100, 500, 500]
:open
:closed
nil
Source: Range#===
when ..0 and when 31.. are a beginless and an endless range; they are what replaces the open-ended else if ladders
written with comparison operators. The splat form when *OPEN_STATES tests each element in turn, so a constant array of
allowed values can be shared between a case and a validation.
Showing === at work
Because when is nothing but ===, you can call it yourself and you can define it. A class that defines a
class-level === can be used as a when candidate, which is how value objects, custom matchers and small DSLs
plug into case.
# frozen_string_literal: true
# rubocop:disable Style/CaseEquality
range = 1..5
squared = ->(n) { n > 2 }
seen = :a
p Integer === 3
p range === 3
p(/ab/ === "cab")
p seen === :a
p squared === 3
p Set[1, 2] === 2
# rubocop:enable Style/CaseEquality
title = "Shelf 2.0"
case title
when /(\d)\.(\d)/
p [Regexp.last_match(1), Regexp.last_match(2)]
end
# when with a custom ===
class Even
def self.===(other) = other.respond_to?(:even?) && other.even?
end
number = 4
p(case number when Even then "even" else "odd" end)
true
true
true
true
true
true
["2", "0"]
"even"
Source: Object#===
RuboCop’s Style/CaseEquality discourages writing === explicitly outside when; the example disables it because showing the
operator is the point. Note that Regexp#=== sets the match globals just like =~; since $~ and $1 are
frame-local and easy to overwrite, prefer Regexp.last_match(1) or named captures, as above (RuboCop’s
Style/PerlBackrefs).
case without a subject
When case has no subject, each when holds an arbitrary condition, and the first truthy one wins. It reads like an
if/elsif chain with aligned tests, and it is the right choice when every branch tests something different.
# frozen_string_literal: true
def describe(copies, reserved)
case # rubocop:disable Style/EmptyCaseCondition
when copies.zero? then "none left"
when reserved >= copies then "all reserved"
when copies > 5 then "plenty"
else "available"
end
end
puts describe(0, 0)
puts describe(2, 2)
puts describe(9, 0)
puts describe(3, 1)
# a case with no matching branch and no else is nil
number = 1
p(case number when 2 then :two end)
# the value of case can be assigned directly
file = "x.rb"
kind =
case file
when /\.rb\z/ then :ruby
else :other
end
p kind
none left
all reserved
plenty
available
nil
:ruby
RuboCop’s Style/EmptyCaseCondition prefers an if chain; the style choice is local to a project. In a subject-less case, a
comma-separated list is "or" as well.
Duplicates and ordering
With warnings enabled, Ruby reports a when whose literal candidate repeats an earlier one, because that branch can never run:
w1.rb:7: warning: 'when' clause on line 7 duplicates 'when' clause on line 5 and is ignored
Source: Warning
Beyond that, the order of branches is yours to get right: a class candidate such as Numeric in front of Integer
hides the Integer branch without any diagnostic. The implementation compiles a case whose when candidates are all
literals (integers, strings, symbols, nil, true, false) into a hash dispatch instead of a sequence of === calls, as long
as === of those core classes has not been redefined, so a long case over literals costs one hash lookup instead of a chain of comparisons. For
other kinds of candidate the comparisons run in order.
case with in
Since Ruby 2.7 (stable in 3.0) case can also use in clauses for pattern matching, which matches structure
(arrays, hashes, Data, Struct, find patterns, guards) and binds variables, not just ===. case/when and case/in
cannot be mixed in one case. The shelf gem’s Loan#describe is the complete example; the details are in
Pattern Matching.
while and until
while repeats its body as long as the condition is true; until repeats it as long as the condition is false. The
condition is tested before the first pass (zero iterations are possible), do is optional (and rare), and the value of
the loop is nil unless break supplies one.
# frozen_string_literal: true
copies = 3
while copies.positive?
puts "#{copies} copies left"
copies -= 1
end
# until is while with the condition negated
attempts = 0
until attempts >= 2
attempts += 1
puts "attempt #{attempts}"
end
# the value of a loop is nil unless break supplies one
limit = 5
n = 0
result =
while n < limit
n += 1
break :done if n == 3
end
p result
p(while n > limit do end)
# 'do' is optional; here it keeps a one-line body readable
queue = [1, 2, 3]
puts queue.shift * 2 while queue.any?
3 copies left
2 copies left
1 copies left
attempt 1
attempt 2
:done
nil
2
4
6
Source: Control Expressions: while Loop
while and until also exist as modifiers, which is the form used for one-line loops: counter += 1 while counter < 10
(the first example in the modifier section). RuboCop’s Style/WhileUntilModifier prefers it for a single-statement body.
Style/InfiniteLoop suggests loop do instead of while true.
begin … end while
When the loop modifier is attached to a begin ... end block, the body runs once before the condition is tested, like a
do ... while loop in C. This is the only case where a modifier tests after the body.
The example under Modifier if and unless shows it: the condition is
runs.negative?, false from the start, yet the body ran once. The documentation notes that without a rescue or ensure
Ruby optimises away the exception-handling overhead. Rubocop’s Lint/Loop recommends loop do ... break if ... end for
this shape, which is more explicit and also works with next. A begin block that has a modifier if or unless is an
ordinary conditional that tests first.
for
for iterates by calling each: for x in collection. It differs from each in one important way: it does not create
a new variable scope, so the loop variable (and any variable assigned in the body) is still visible after the loop. The
value of a for expression is the collection iterated, unless break supplies another value. The guide’s verdict is that
it is rarely used in modern Ruby code, and RuboCop’s Style/For flags it.
# frozen_string_literal: true
# rubocop:disable Style/For
titles = %w[Dune Emma]
for title in titles do
puts title
end
puts "title is still visible: #{title}"
%w[Hamlet Ulysses].each do |name|
puts name
end
puts defined?(name).inspect
# for returns the thing it iterated
result = (for i in 1..2 do i end)
p result
# for destructures like a block would
for isbn, copies in { "111" => 2, "222" => 5 } do
puts "#{isbn}: #{copies}"
end
# rubocop:enable Style/For
Dune
Emma
title is still visible: Emma
Hamlet
Ulysses
nil
1..2
111: 2
222: 5
Source: Control Expressions: for Loop
title is still defined after the for, name is undefined after the each (defined?(name) returns nil). This
leakage is the common source of bugs: an i from a for can overwrite an outer i, and a closure created in the body
captures the one shared variable, not one per pass. The same loop with each gets a fresh binding on every iteration.
Use for only where you need exactly its scope behaviour or its brevity in a throwaway script, never in a library.
loop and StopIteration
Kernel#loop repeats its block forever, which makes it the Ruby spelling of an infinite loop. It exits in three ways:
break (with an optional value, which loop returns), an exception, or a StopIteration. loop rescues
StopIteration itself and returns the exception’s result. That makes it the standard way to drain an external
enumerator:
# frozen_string_literal: true
numbers = [1, 2, 3].each
loop do
puts numbers.next
end
# loop returns the StopIteration result
queue = [10, 20].each
outcome = loop { queue.next }
p outcome
# break gives loop a value
count = 0
found = loop do
count += 1
break count * 100 if count == 3
end
p found
# an enumerator's own StopIteration
e = [1].each
e.next
begin
e.next
rescue StopIteration => e
puts "#{e.class}: #{e.message}"
p e.result
end
1
2
3
[10, 20]
300
StopIteration: iteration reached an end
[1]
Source: Kernel#loop
Enumerator#next raises StopIteration ("iteration reached an end") after the last element; the exception’s
result is the return value of the underlying iteration method, which is why the second example returns [10, 20]
(the result of Array#each). Without loop, you have to rescue StopIteration explicitly, as the last lines show. See
Enumerators and Lazy Evaluation for external versus internal
iteration. The exception class is documented at StopIteration#result.
loop called without a block returns an enumerator (an infinite one, Enumerator#size is Float::INFINITY), so it can
feed lazy pipelines. A raised error that is not a StopIteration propagates normally and terminates the loop.
Iterating over numbers and collections
Counting loops are better expressed as an iterator method than as while with a manual counter. These are the idioms that replace the
C-style for (i = 0; i < n; i++):
# frozen_string_literal: true
3.times { print it, " " }
puts
1.upto(3) { print it, " " }
puts
3.downto(1) { print it, " " }
puts
1.step(10, 4) { print it, " " }
puts
10.step(1, -4) { print it, " " }
puts
0.5.step(by: 0.5, to: 2.0) { print it, " " }
puts
%w[a b c].each_with_index { |s, i| print "#{i}:#{s} " }
puts
p((1..3).each_with_object([]) { |i, acc| acc << (i * i) })
# the loop equivalents
total = 0
(1..4).each { total += it }
p total
p((1..4).sum)
p((1..4).reduce(:*))
0 1 2
1 2 3
3 2 1
1 5 9
10 6 2
0.5 1.0 1.5 2.0
0:a 1:b 2:c
[1, 4, 9]
10
10
24
Source: Integer#times, Integer#upto, Integer#downto, Numeric#step
times, upto, downto and step are the counting loops (step accepts a negative step and, for floats, by: and to:
keywords); each_with_index and each_with_object carry an index or an accumulator. Inside these blocks, it (Ruby 3.4) or _1 name the
single parameter. For algorithms that really are index arithmetic (two pointers, binary search, a sieve), a while loop
is still the clearest tool.
The collection methods are chosen by what the loop computes, not by how it iterates:
| You want … | Use |
|---|---|
a transformed copy |
|
only some elements |
|
the first element that satisfies a test |
|
a boolean over the whole collection |
|
one accumulated value |
|
a side effect per element |
|
Source: Enumerable
A loop that builds an array by each plus <<, or that searches with each and a flag, is almost always a map,
select or find waiting to be written. Two rules from the "extract the block body" refactoring keep these readable: when a
block exceeds a handful of lines, name it as a method; and use the method that states the intent (find instead of
select(...).first). The Enumerable methods are on their own page, Enumerable.
break, next and redo
Three keywords alter a loop or a block from inside. They work identically in while/until/for and in blocks
passed to iterators (the exact behaviour inside a proc versus a lambda is on
Blocks, Procs and Lambdas).
| Keyword | Effect | Value it can carry |
|---|---|---|
|
leaves the loop, or the method that yielded to the block, immediately |
the result of the loop or of the iterator call; |
|
ends the current iteration and starts the next one |
the result of the current block call (for example, the element |
|
restarts the current iteration without re-testing the condition or advancing the iterator |
none |
|
only legal inside a |
none |
# frozen_string_literal: true
copies = [3, 0, 2, 5]
# break leaves the iterator and can supply its value
first_missing = copies.each_with_index do |n, i|
break i if n.zero?
end
p first_missing
# when the iterator finishes normally, the value is the iterator's own
p(copies.each { |n| break n if n > 100 })
# next skips to the following iteration and can supply the block value
labelled = copies.map do |n|
next :none if n.zero?
n * 2
end
p labelled
skipped = copies.map do |n|
next if n.zero?
n * 2
end
p skipped
# break out of a while loop
i = 0
stopped =
while i < 10
i += 1
break "stopped at #{i}" if i == 4
end
p stopped
# next in a while loop re-tests the condition
odds = []
n = 0
while n < 8
n += 1
next if n.even?
odds << n
end
p odds
1
[3, 0, 2, 5]
[6, :none, 4, 10]
[6, nil, 4, 10]
"stopped at 4"
[1, 3, 5, 7]
What the example shows:
-
break valuebecomes the result of the whole iterator call (each_with_indexhere) or loop. Abreakwithout a value makes the call returnnil. If the iterator is not interrupted, its normal result is returned instead:eachreturns the receiver, which is why the second line prints the whole array; -
nextis Ruby’scontinue, andnext valuesupplies the block’s result. A barenextyieldsnil, somap { next if x.zero?; x * 2 }leavesnilholes in the result; usefilter_maporrejectwhen you wanted to skip elements; -
in a
whileloopnextjumps to the condition check, so a counter updated afternextcan be skipped; update it before.
redo
redo repeats the current iteration with the same element, without testing the condition. It is rare and error-prone,
since it is easy to loop forever, but it exists for retry-style loops that must not advance. The first example restarts
an iteration for the first item twice; the second is the example from the language guide.
# frozen_string_literal: true
attempts = Hash.new(0)
%w[a b].each do |item|
attempts[item] += 1
redo if item == "a" && attempts[item] < 3
end
p attempts
# the example from the language guide
result = []
while result.length < 10
result << result.length
redo if result.last.even?
result << (result.length + 1)
end
p result
{"a" => 3, "b" => 1}
[0, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11]
redo is not the same as retry. In Ruby 1.8 either could be used for this, but since Ruby 1.9 retry is only valid in
a rescue clause, where it re-runs the begin block (see
Exceptions and Error Handling); anywhere else it is a
syntax error:
w4.rb: --> w4.rb
Invalid retry without rescue
> 3 retry
w4.rb:3: syntax error found (SyntaxError)
1 | # frozen_string_literal: true
2 |
> 3 | retry
| ^~~~~ Invalid retry without rescue
Interaction with ensure
break, next, redo and return all run pending ensure clauses on the way out. Since Ruby 2.5 a do ... end block can
have its own rescue/ensure, which is the neat way to attach cleanup to each iteration:
# frozen_string_literal: true
def scan(items)
items.each do |item|
break item if item > 1
ensure
puts "checked #{item}"
end
end
p scan([1, 2, 3])
checked 1
checked 2
2
Source: Exceptions
The ensure of the iteration that executed break still printed ("checked 2"), and the value of the break was returned.
break, next and return in blocks, procs and lambdas
Inside a block, break leaves the method that received the block, next leaves the block only, and return returns
from the method that defined the block. A proc stored and called after its defining method has finished cannot break
out of anything, and that situation raises LocalJumpError. A lambda treats return and break like next (they leave
the lambda only).
# frozen_string_literal: true
pr = proc { break 1 }
begin
pr.call
rescue LocalJumpError => e
puts "#{e.class}: #{e.message}"
p e.reason
end
def helper = yield
p(helper { next 5 })
lam = -> { break 7 }
p lam.call
def first_even(numbers)
numbers.each { |n| return n if n.even? }
nil
end
p first_even([1, 4, 5])
LocalJumpError: break from proc-closure
:break
5
7
4
Source: LocalJumpError
LocalJumpError#reason is :break here; the class page lists the other values. The full account of
return/next/break in blocks, procs and lambdas, with a diagram, is on
Blocks, Procs and Lambdas.
Leaving nested loops
Ruby has no labelled break. break leaves only the innermost loop, so leaving two levels at once needs another
mechanism. In order of preference:
-
extract the search into a method and
returnfrom it (it also gives the search a name); -
use
catch/throwwhen the exit is an expected outcome of the algorithm and extracting a method is awkward; -
let an Enumerable method say it (
find,any?,flat_map ... first), because the iterator stops by itself; -
only if nothing else fits, set a flag and test it in each loop.
# frozen_string_literal: true
GRID = [[1, 2, 3], [4, 5, 6], [7, 8, 9]].freeze
# 1. extract a method and return from it
def find_cell(grid, target)
grid.each_with_index do |row, y|
row.each_with_index do |value, x|
return [x, y] if value == target
end
end
nil
end
p find_cell(GRID, 6)
# 2. catch/throw for an expected early exit from deep inside
position = catch(:found) do
GRID.each_with_index do |row, y|
row.each_with_index { |value, x| throw :found, [x, y] if value == 8 }
end
nil
end
p position
# 3. often an Enumerable method says it directly
first_big = GRID.flatten.find { it > 4 }
p first_big
[2, 1]
[1, 2]
5
Source: Kernel#catch
catch and throw
catch and throw are the non-local jump for control flow that is expected, as opposed to exceptions, which are for
failures. catch(tag) { ... } runs the block; a throw tag, value anywhere inside it, however deeply nested through
method calls, unwinds the stack up to the matching catch and makes that catch return value (nil if omitted). If no
throw happens, catch returns the block’s value. The tag is compared with identity (equal?), so symbols and objects
work; strings do not match across different objects. Without an argument, catch creates a fresh Object and passes it to
the block, which makes the tag unforgeable.
# frozen_string_literal: true
require "shelf"
SHELVES = [[1, 2], [3, 4], [5, 6]].freeze
found = catch(:found) do
SHELVES.each do |shelf|
shelf.each do |n|
throw :found, n if n * n > 10
end
end
:nothing
end
p found
# without a matching throw, catch returns the block value
p(catch(:x) { :normal })
# the tag can be any object; a fresh Object is private to the block
r = catch do |tag|
[1, 2, 3].each { |n| throw tag, n * 10 if n == 2 }
:unreached
end
p r
# a throw to a tag nobody catches is an error
begin
throw :nobody
rescue UncaughtThrowError => e
puts "#{e.class}: #{e.message}"
p e.tag, e.value
end
4
:normal
20
UncaughtThrowError: uncaught throw :nobody
:nobody
nil
Source: Control Expressions: throw/catch
A throw whose tag no active catch matches raises UncaughtThrowError (a subclass of ArgumentError), with the tag and value readable from the exception. That
is deliberate: it tells you that the catch is missing rather than silently doing nothing. ensure clauses between the
throw and the catch run as the stack unwinds, exactly as for exceptions.
catch and throw are ordinary methods of Kernel, not keywords. Inside a BasicObject subclass, which does not include Kernel,
call ::Kernel.catch and ::Kernel.throw. They are used by gems whose parsers or tree walkers stop early (for example,
Ruby’s own Find.prune, and Sinatra’s halt), but in application code a return from an extracted method usually
reads better. See Kernel#throw and
UncaughtThrowError.
Statements and expressions
Ruby’s grammar distinguishes statements from expressions. Every expression is a statement, but not every statement is an
expression. In their standard forms, if, unless, while, until and begin are expressions. The modifier forms
of if, unless, while, until and rescue are statements that are not expressions. Contexts that need an
expression, such as a method argument, therefore reject a modifier form unless you add parentheses:
w3.rb:3: syntax errors found (SyntaxError)
1 | # frozen_string_literal: true
2 |
> 3 | puts(1 if true)
| ^~ unexpected 'if'; expected a `)` to close the arguments
| ^ unexpected ')', ignoring it
| ^ unexpected ')', expecting end-of-input
# frozen_string_literal: true
ready = true
# a modifier if is a statement, not an expression: wrap it in parentheses
puts((1 if ready))
value = (2 unless ready)
p value
# a plain if ... end is an expression and needs no extra parentheses
puts(if ready then 3 end)
1
nil
3
puts (1 if ready) with a space is also accepted: it is read as a call without parentheses whose argument is a
parenthesised statement. The double parentheses are the clearer spelling. The same
split decides how a modifier rescue groups: a if b rescue c is (a if b) rescue c, because b rescue c is a statement
and cannot be the test of an if modifier. Modifier rescue has higher precedence than =, and modifier if lower, so
stmt if v = expr rescue x is stmt if v = (expr rescue x).
The flip-flop
A range literal used as a condition (inside if, unless, while, until, ?:, !, not, and the modifier
forms) is not a Range at all but a flip-flop: a hidden boolean that turns on when its left condition becomes true,
stays on while the right condition is false, and turns off when it is true. It was designed for one-liners that
select a block of lines (ruby -ne 'print if /BEGIN/../END/').
# frozen_string_literal: true
# rubocop:disable Lint/FlipFlop
lines = %w[intro BEGIN one two END outro]
inside = lines.select { |l| true if (l == "BEGIN")..(l == "END") }
p inside
# three dots: the end condition is not tested on the line that turned it on
p((0..5).select { |v| true if (v == 2)...(v == 2) })
p((0..5).select { |v| true if (v == 2)..(v == 2) })
# rubocop:enable Lint/FlipFlop
["BEGIN", "one", "two", "END"]
[2, 3, 4, 5]
[2]
Source: Control Expressions: Flip-Flop
With two dots (..) the off condition is tested on the same pass in which the flip-flop turned on, so
(v == 2)..(v == 2) selects just [2]. With three dots the off condition is only tested starting on the next pass, so
(v == 2)...(v == 2) never turns off and selects [2, 3, 4, 5]. The state is hidden in the method frame (it is a
hidden local variable), which makes the flip-flop a source of surprises in methods called more than once and in code
that uses threads. RuboCop’s Lint/FlipFlop flags it. Use a plain boolean variable (inside = true if ...) for readable
code; the precedence of the range operators is on
Operators and Precedence.
BEGIN, END and at_exit
BEGIN { ... } runs its block before anything else in the file, and END { ... } runs its block when the program
exits. Both require braces (do ... end is a syntax error), BEGIN must be at the top level, and an END block registers
itself once, the first time the statement is executed.
# frozen_string_literal: true
# rubocop:disable Style/EndBlock, Style/BeginBlock
puts "main body"
END { puts "END block runs last" }
at_exit { puts "at_exit handler" }
END { puts "second END (runs first: reverse order)" }
BEGIN { puts "BEGIN block runs first" }
# rubocop:enable Style/EndBlock, Style/BeginBlock
BEGIN block runs first
main body
second END (runs first: reverse order)
at_exit handler
END block runs last
END blocks and Kernel#at_exit handlers share one list that runs in reverse order of registration, as the output
shows (the second END, registered last, ran first). END inside a method warns, since each call would register
another handler:
w6.rb:4: warning: END in method; use at_exit
Source: Kernel#at_exit
In practice, BEGIN and END are for ruby -n/-p one-liners (for example, ruby -ne 'BEGIN { count = 0 }; END { puts count }; count += gets.to_i'
sums numbers from standard input). In a program, use at_exit (RuboCop’s Style/EndBlock and Style/BeginBlock say the same). exit
and abort raise SystemExit so that ensure clauses and exit handlers still run;
see Ruby Command Line and Environment for the
-n and -p switches and Exceptions and Error Handling
for SystemExit.
Diagnostics at a glance
| You see | Cause | Fix |
|---|---|---|
|
a variable assigned in the test of a modifier |
assign before, or use |
|
|
use |
|
a literal where a test was meant |
remove it or test a real expression |
|
the same literal twice in a |
merge or delete the duplicate |
|
|
restructure with |
|
|
use |
|
a proc calls |
use |
|
|
wrap the call in |
|
|
add a branch or |
|
|
use |
Source: Warning
Style and pitfalls
-
Prefer expressions over assignments in every branch.
label = if cond then a else b endbeats assigninglabelin both arms. -
Prefer guard clauses over nesting, and iterators over index loops.
-
Do not use
and/orin conditions: precedence differs from&&/||and=; use them, if at all, only as flow control. -
Do not test with
== trueor== false; test the value (truthiness) or use the predicate. -
Do not depend on
nilas a "not found" marker ifnilcan be a valid value; usefetchwith a default or aKeyError(see Hashes). -
Do not modify a collection while iterating over it with
each/while; iterate over a copy (dup) or build a new one. -
A
whileloop does not create a scope, a block does. A variable first assigned inside awhileis visible after it; one first assigned inside a block is not. -
loophides an exit condition in the body, so state it clearly at the top (break if done?), or useuntilwhen the exit is the loop’s purpose. -
caseover a class hierarchy is a smell when it grows: it is anif-ladder on type that polymorphism (a method on each class) replaces; see SOLID and Design Patterns.
Version notes
| Version | Change relevant to this page |
|---|---|
Ruby 2.6 |
endless ranges ( |
Ruby 2.7 |
beginless ranges ( |
Ruby 3.0 |
pattern matching stable for |
Ruby 3.1 |
one-line |
Ruby 3.2 |
find pattern stable |
Ruby 3.4 |
the |
Ruby 4.0 |
a line starting with |
|
Source: NEWS for Ruby 4.0.0
Where older books and articles are outdated
Material written for Ruby 2.x or early 3.x remains right about control flow, which has hardly changed, but a few details are dated:
-
examples of error output with
undefined method, with straight quotes and the receiver described asx' for nil:NilClassand an unquoted backtick: since Ruby 3.4 the text is `undefined method 'x' for nilniloran instance of Foo; -
_1-only loop and block examples: Ruby 3.4 addedit, shown in the examples above, which reads better for a single parameter (_1is still valid, but not combinable withit); -
hashes printed as
{:name=>"Ico"}: since Ruby 3.4,inspectprints{name: "Ico"}and{"a" => 3}, with spaces around=>; -
guidance that predicate methods must return
trueorfalse, and that!always means "mutates": neither is a rule of the language (see the truthiness section); -
pattern matching presented as new in 2.7: it is stable since 3.0, with the find pattern stable in 3.2;
These points are only corrections; the rest of the catalogue of constructs in this page is unchanged since Ruby 1.9.
References
-
Control Expressions: the language guide for
if,unless,case, loops,break/next/redo, modifiers, flip-flop andthrow/catch -
Miscellaneous Syntax:
BEGINandEND,defined? -
Kernel (
loop,catch,throw,at_exit,exit), and the exceptions StopIteration, UncaughtThrowError, LocalJumpError and NoMatchingPatternError -
NEWS for Ruby 4.0.0: the leading logical operators
-
The Ruby Style Guide and RuboCop Style cops:
Style/AndOr,Style/For,Style/NestedTernaryOperator,Style/UnlessElse,Style/CaseEquality