---
title: 'Control Flow'
source: 'https://academia.sh/en/courses/ruby-fundamentals/control-flow'
course: 'Ruby Fundamentals'
language: en
updated: '2026-10-21T05:03:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows with an owner measurement that case sends the === message to each when branch'
    - 'Separates if and while as syntax rather than messages by using defined?'
    - 'Finds the responder of case as the class of the branch object, and shows that statement modifiers and if produce a value'
---

# Control Flow

The case expression sends the === message to each when branch, while if and while are syntax that sends no message; statement modifiers and the value of if; the responder of case is the class of the branch object.

The previous lesson measured that assignment is not a message but the language's own spelling, and it left a question: of `if`, `while`, and `case`, which ones work with the language's own spelling, and which one really sends a message? The concepts of conditions and loops were introduced in the Programming Fundamentals course (M01/K02); they are not repeated here. This lesson measures only one question: when a `case` branch is chosen, which message goes to which object?

In the Rust course (M10/K01), the question "does this construct produce a value" was measured. In Ruby, nearly every control structure produces a value, and that question returns in this lesson only as a side observation. The main measurement is the message behind the branching.

## case Sends === to Each Branch

The file below sends the `===` message to a range object and looks at who answers it. Then it puts the same range in a `when` branch inside a `case`.

```ruby
# range_match.rb — case sends the === message to each when branch
range = 1..10

puts range.class
puts range.method(:===).owner
puts range === 7

result = case 7
         when 1..10 then "inside"
         else "outside"
         end
puts result
```

```
Range
Range
true
inside
```

`1..10` is a `Range` object, and `Range` answers the `===` message. When the range is asked about `7`, the reply is `true`. When `case 7 when 1..10` is written, the branch matches as well: `case` makes its value the argument of the `===` message sent to the object of the `when` branch. So `when 1..10` is, behind the scenes, the question `(1..10) === 7`.

**LB10.** In this lesson, the "branch object" is the object written on the `when` line, and the "case value" is the object that comes after the `case` keyword. The receiver of the `===` message is the branch object, and its argument is the case value. The order matters: if `7 === (1..10)` were written, the receiver would be `7` and the responder would change.

## if and while Send No Message

We saw that the `===` message goes out in the `case` branches. The same question can be asked about `if` and `while`: is a condition chosen by a message? `defined?` answers this directly.

```ruby
# condition_expressions.rb — if and while are syntax
puts defined?(if true then 1 end)
puts defined?(while false do end)
puts defined?(1 + 2)
puts defined?(case 1 when 1 then 2 end)
```

```
expression
expression
method
expression
```

`if` and `while` were reported as `"expression"`; that is, they are structures of the language, not method calls. `1 + 2`, on the other hand, is `"method"`. `case` also came out as `"expression"`: the branching itself is syntax, but each `when` comparison inside it sends a message. There are two layers in the same structure: the `case` skeleton is syntax, and the choice of a branch is a message answered by the class of an object.

## Statement Modifiers and the Value of if

In Ruby, a condition or a loop can be put at the end of a sentence. These are called statement modifiers: `puts x if y`, `x += 1 until z`. In addition, `if` produces a value; when no branch runs, that value is `nil`.

```ruby
# statement_modifier.rb — statement modifiers and the value of if
x = 3
puts "big" if x > 2
x += 1 until x >= 6
puts x

y = if false then 1 end
p y
z = if true then 1 end
p z
```

```
big
6
nil
1
```

The first line ran because `x > 2` was true. The `until` modifier repeated the addition until `x` reached six. `if false then 1 end` returned `nil` because it ran no branch, and `if true then 1 end` returned `1`. The result: `if` is not a statement but an expression that produces a value. This is the same property as an assignment carrying the value `7`.

## Truthiness and the Value of a Loop

Whether a condition counts as true or false is also a rule of the language. In Ruby, only two values count as false: `nil` and `false`. Everything else, including `0` and the empty string, counts as true. The file below tries five values in the same condition.

```ruby
# truthiness.rb — only nil and false count as false
[nil, false, 0, "", []].each do |value|
  status = if value then "truthy" else "falsy" end
  printf("%-5s %s\n", value.inspect, status)
end
```

```
nil   falsy
false falsy
0     truthy
""    truthy
[]    truthy
```

`nil` and `false` came out false, and `0`, `""`, and `[]` came out true. This rule is the language's own: only `nil` and `false` count as false, and every other value is true. Loops produce values too. If a `while` loop exits with `break` and a value, that value becomes the result of the loop; if it ends without `break`, the result is `nil`.

```ruby
# loop_value.rb — while produces a value too
n = 0
result = while true
  n += 1
  break n * 10 if n == 3
end
p result

empty = while false do end
p empty
```

```
30
nil
```

The loop exited on the third round with `30`, and `result` carried this value. The loop that never ran gave `nil`. This is the same behavior as `if` producing `nil` when no branch runs: in Ruby, control structures are expressions and have values.

## Loops and the Values They Produce

The earlier sections measured branches; this section measures loops. The question is again about values: does a loop expression produce a value, and can `break` determine that value? The responder axis appears in two places in this section: `while` and `until` are keywords and syntax, while `loop` is a method and its owner is read from the output. The second block looks at a scope difference: does a variable of `for` behave the same way toward the outside as a block variable?

```ruby
# loops.rb — the value of loops and next
result = loop { break 5 }
puts result
i = 0
i += 1 while i < 3
puts i
j = 10
j -= 1 until j < 8
puts j
empty = while false do end
puts empty.inspect
total = 0
(1..5).each do |n|
  next if n.even?
  total += n
end
puts total
puts method(:loop).owner
```

```
5
3
7
nil
9
Kernel
```

`loop { break 5 }` gave `5` as the value of the loop: the argument of `break` becomes the result of the loop expression. The modifier `i += 1 while i < 3` raised `i` to `3`, and `j -= 1 until j < 8` stopped `j` at `7`; `while` continues as long as the condition stays true, and `until` as long as the condition stays false. The expression `while false do end` produced a value although it never ran, and this value is `nil`. So a loop, like `if`, is an expression and carries a value. Inside `(1..5).each`, `next if n.even?` skipped the even numbers; the remaining `1`, `3`, and `5` were added up and `9` came out. `next` skips an iteration and `break` ends the loop; both are written inside a block and both are keywords of the language; this file printed no owner for them. The last line showed that the owner of `loop` is `Kernel`: `loop` is not a keyword but a method, like `puts`.

```ruby
# for_leak.rb — the for variable leaks out, the block variable does not
[1, 2].each { |k| k }
d = defined?(k)
puts d.inspect
for m in [1, 2] do end
puts defined?(m)
puts m
puts [1, 2].method(:each).owner
puts 1.upto(3).to_a.inspect
puts 3.times.class
```

```
nil
local-variable
2
Array
[1, 2, 3]
Enumerator
```

When the variable `k` of the `each` block was asked about from outside, it returned `nil`: a block variable stays inside the block. `for m in [1, 2] do end`, on the other hand, leaked `m` to the outside: `defined?(m)` reported `"local-variable"`, and in the end `m` was `2`. So a `for` loop does not open a new scope. The scope behavior of two spellings that do the same job differs; this difference turns the choice between them from a matter of taste into a matter of consequence. `[1, 2].method(:each).owner` gave `Array`; that is, the class of the array answers `each`. `1.upto(3).to_a` gave `[1, 2, 3]`, and `3.times`, called without a block, returned an `Enumerator`: `times` called without a block gives an iterator instead of producing a value.

**LB11.** That `for` calls the `each` message of the array behind the scenes was not measured. What was measured is only the scope difference and that the owner of `each` is `Array`.

## Short-Circuiting and Comparison

Conditions are not built only with `if` and `case`: the `||` and `&&` operators also decide whether to run the right side. This section measures two questions: what is observed when an operator skips evaluating the right side, and who owns the methods that do comparison? The axis is again the responder; `||` and `&&` are syntax of the language, while `<=>` and `between?` are messages.

```ruby
# short_circuit.rb — does the right side run, and who answers
def visit(name)
  puts name
  true
end
visit("a") || visit("b")
visit("c") && visit("d")
v = nil || "fallback"
puts v
w = false && 1
puts w.inspect
puts 1 <=> 2
puts 5.between?(1, 10)
n = 5
verdict = n > 3 ? "big" : "small"
puts verdict
puts 5.method(:<=>).owner
puts 5.method(:between?).owner
```

```
a
c
d
fallback
false
-1
true
big
Integer
Comparable
```

On the line `visit("a") || visit("b")`, only `a` was printed: because the left side was true, the right side never ran. On the line `visit("c") && visit("d")`, both `c` and `d` were printed, because `&&` also looks at the right side when the left side is true. This is short-circuiting: if the result is already determined by the left side, the right side is not evaluated. The expression `nil || "fallback"` gave `fallback`; `||` does not only produce a truth value, it returns the operand itself. `false && 1` returned `false`. Giving a default value with `||` relies on this behavior: if the left side is `nil` or `false`, the value on the right returns.

`1 <=> 2` gave `-1`: the comparison message reports the ordering as an integer. `5.between?(1, 10)` was `true`. The ternary operator `n > 3 ? "big" : "small"` gave `big`. The owner lines revealed two different responders: `Integer` answers the `<=>` message, while the `Comparable` module answers the `between?` message. Both are sent to a number and give a result, but the places where they are defined differ. So the comparison messages used when writing conditions are divided between the methods of the class itself and the methods of a shared module.

## Error Flow: rescue, else, and ensure

Control flow does not consist of conditions and loops alone; an error changes the flow as well. This section measures the order in which the `rescue`, `else`, and `ensure` branches run, and which of them determines the value of the method. The error class is printed, its message is not. The axis is syntax: `rescue` and `ensure` are keywords, while `raise` is a method.

```ruby
# exception_flow.rb — the order of rescue, else, and ensure
def guarded
  yield
rescue ZeroDivisionError => e
  puts e.class
  :rescued
else
  :clean
ensure
  puts "cleanup"
end
a = guarded { 1 / 0 }
puts a.inspect
b = guarded { 1 / 1 }
puts b.inspect
c = begin
  Integer("x")
rescue ArgumentError
  -1
end
puts c
puts method(:raise).owner
puts method(:guarded).owner
```

```
ZeroDivisionError
cleanup
:rescued
cleanup
:clean
-1
Kernel
Object
```

In the first call, the block computed `1 / 0` and raised `ZeroDivisionError`. The `rescue` branch ran, printed the class, and returned `:rescued`; then the `ensure` branch wrote `cleanup`. The return value stayed `:rescued`: `ensure` does not determine the value of the method. In the second call no error occurred, so the `else` branch ran and the value of the method became `:clean`; `ensure` again wrote `cleanup`. So `ensure` runs on both paths, `rescue` only when there is an error, and `else` only when there is none.

A `begin` block is also an expression and carries a value. `Integer("x")` raised `ArgumentError`, the `rescue` branch returned `-1`, and `c` took this value. This is the counterpart, in error flow, of `if` producing a value: one of the branches runs, and the last expression of the branch that ran becomes the value of the whole. The owner lines showed `Kernel` for `raise` and `Object` for `guarded`: raising an error is a method call, but `rescue` and `ensure` are the language's own spelling. The text of the error message was not printed in this section, because the text of a message may change between versions, while its class does not.

## The Bounding Measurement

The responder of this lesson is a class. The slip here is to take the responder of `case` to be the class of the value given to `case`. Yet in the expression `case 7`, the class of `7` is `Integer`, but the receiver of the `===` message is the branch object. For this reason, for the same `7`, `Module` answers on the line `when Integer`, `Regexp` on the line `when /a/`, and `Range` on the line `when 1..10`.

```ruby
# branch_object.rb — the responder of case is the class of the branch object
branches = [Integer, /a/, 1..10]

branches.each do |branch|
  printf("%-8s class %-8s owner %s\n", branch.inspect, branch.class, branch.method(:===).owner)
end

puts Integer === 7
puts(/a/ === "table")
puts branches[2] === 7
```

```
Integer  class Class    owner Module
/a/      class Regexp   owner Regexp
1..10    class Range    owner Range
true
true
true
```

Three branch objects, three different owners: for `Integer`, the `Module` class answers the `===` message (the `Integer.===` observation from the previous lesson), for the regular expression `Regexp` does, and for the range `Range` does. Even when the value of `case` is always `7` or the same `"table"`, the only thing that changes is the receiver. What is read as the responder class is the class of the branch object, not of the case value.

The two layers measured in this lesson must be read together. The skeleton of a control structure, that is, the words `if`, `while`, and `case`, is the language's own spelling, and `defined?` reports them as `"expression"`. The comparison inside a branch, on the other hand, is a message, and the class of an object answers it. Separating the two turns the question "why did this branch match" into the question "what does the `===` method of the branch object do" when reading a `case` expression.

## Summary

- `case` sends the `===` message to the branch object in each `when` branch; `when 1..10` is, behind the scenes, the question `(1..10) === 7`.
- The receiver of the `===` message is the branch object and its argument is the case value; if the order is reversed, the responder changes.
- `if` and `while` are reported as `"expression"` by `defined?`; they are structures of the language and send no message.
- Statement modifiers (`if`, `unless`, `while`, `until`) make a sentence conditional or looped; `if` produces a value, and returns `nil` when no branch runs.
- The responder of this lesson is a class: the responder of `case` is the class of the branch object, not of the case value.
- `loop { break 5 }` returns `5` and `while false do end` gives `nil`; the `for` variable leaks out and the block variable does not; `||` and `&&` do not run the right side unless needed, and `ensure` runs on every path.

## Next Step

We measured who answers the condition and the choice of branch. But when the receiver is never written in a method call, for example when `puts` or `format` is written, who handles the message? The next lesson, Methods, measures the responder of a receiverless call.
