---
title: 'Control Flow'
source: 'https://academia.sh/en/courses/rust-fundamentals/control-flow'
course: 'Rust Fundamentals'
language: en
updated: '2026-09-15T04:58:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Classifies by running it which of the if, match, loop, while, for forms produce a value as an expression'
    - "Shows that all of if's and match's branches must give the same type, and that a labeled break can carry a value out of a nested loop"
    - 'Proves that an if without else is also an expression, and that its value is ()'
---

# Control Flow

if, match, loop, while, for — of the five control-flow forms, only three can produce a value: if and match are each an expression and require all their branches to give the same type, loop gives a value only through break, while and for never do; a labeled break can carry a value directly out of nested loops.

The previous lesson already used the `if`/`else if`/`else` chain as an expression, but it
showed the state of only one of these five control-flow forms (`if`, `match`, `loop`, `while`,
`for`). This lesson runs all five side by side and asks each of them a single question: does
this form produce a value where it is used, or does it only do a piece of work?

These five forms are grouped under a single category in most languages — "control-flow
constructs" — because all of them determine which lines of a program run in what order. Rust
preserves this common function, but further splits the five into two: some forms also produce
a value while directing flow, some forms only direct flow. This split is not arbitrary — it
stands on the same axis as the question the course has asked from the start, and it is answered
once and for all in this lesson across the five forms.

## Five Forms, Three Answers

```rust
// control_flow.rs — how many of the five forms produce a value
fn main() {
    let number = 7;

    let if_result = if number % 2 == 0 { "even" } else { "odd" };
    println!("if: {}", if_result);

    let match_result = match number {
        0 => "zero",
        n if n % 2 == 0 => "even",
        _ => "odd",
    };
    println!("match: {}", match_result);

    let mut counter = 0;
    let loop_result = loop {
        counter += 1;
        if counter == 5 {
            break counter * 10;
        }
    };
    println!("loop: {}", loop_result);

    let mut total = 0;
    let while_result = while total < 3 {
        total += 1;
    };
    println!("while: {:?}", while_result);

    let mut list_total = 0;
    let for_result = for element in [1, 2, 3] {
        list_total += element;
    };
    println!("for: {:?}", for_result);
    println!("list_total: {}", list_total);
}
```

```
if: odd
match: odd
loop: 50
while: ()
for: ()
list_total: 6
```

The five forms give three separate answers. `if` and `match` each produce a value —
whichever branch is selected, they give that branch's value. `loop` also produces a value, but
only when a value is given to `break`; here, once `counter` reaches five, `break counter * 10`
runs, and `loop` itself turns into the value `50`. `while` and `for`, in turn, never produce a
value — both give `()` in every case, whatever the loop body does. The `list_total` variable
inside the `for` loop makes this clear: the loop's own value is `()`, but a side effect inside
its body (updating an outer variable) does the loop's real work.

These three answers are not randomly distributed; each one's source lies in the form's own
design. `if` and `match` are **selective** — they offer several branches, select exactly one,
and the language's natural choice is to count the selected branch's value as the value of the
whole expression. `loop` is **unbounded** — it does not know on its own when it will stop, so
it can produce a value only when one is explicitly given to `break`; if `break` is never
called, `loop` never ends either, and the question of its "value" does not even arise. `while`
and `for`, in turn, are **conditional** — every turn of the loop depends on a condition (an
explicit boolean expression in `while`, whether an iterator is exhausted in `for`), and the
loop stops on its own when that condition is false. Which turn this natural stop happens on is
not visible from the source, so it would be meaningless for the language to give these two
forms a value — the source itself does not say which state counts as the "result."

This threefold split has one more consequence: because `loop` is unbounded, it can actually set
up the same flow as `while true { }` — neither stops on its own. But only `loop` can produce a
value, `while true { }` cannot; because `while` itself, whatever its condition is, never offers
`break` the ability to carry a value. The same unbounded flow, written with two different
forms, ends up with two different capabilities.

**LB15.** `if` and `match` producing a value comes with one condition: all their branches must
give a value of the **same type**. In the `if_result` example above, both branches give
`&str`; in `match_result`, all three branches give `&str`. If one branch gave a different type
(say, one branch `&str`, another `i32`), the compiler would reject it — because `if_result` or
`match_result` must have a **single** type, and that type must be known at compile time,
regardless of which branch is selected.

The second branch in `match_result` (`n if n % 2 == 0 => "even"`) carries a **match guard**:
the pattern `n` captures `number`'s value, and `if n % 2 == 0` ensures this branch is selected
only when the captured value is even. This is a broader example of pattern matching that will
be taken up separately later; here it is enough to see that an additional condition can decide
whether a branch is selected — its result is still that the branch produces a single value
("even").

This restriction explains another property, also seen in `match`'s branch carrying a match
guard: how many branches a `match` carries and under which conditions the branches are
selected may not be known until runtime, but the branches' **type** is always known at compile
time. The compiler can check the type of all branches without knowing which branch will run,
because type checking looks at the whole of the source, not just the single branch that will
be selected at runtime. This shows how type safety can be ensured independently of which
branch is selected: each branch is checked separately, and then all of them are confirmed to be
the same type — before any of them runs.

## Carrying a Value Out of a Labeled Loop

`loop` producing a value with `break` raises a problem in nested loops: `break` breaks the
nearest enclosing loop, but sometimes a value needs to be pulled out of a loop **further out**.
Rust solves this with loop labels.

```rust
// labeled_loops.rs — a value carried out from a nested loop
fn main() {
    let target = 6;
    let mut i = 1;
    let result = 'outer: loop {
        let mut j = 1;
        loop {
            if j > 5 {
                break;
            }
            if i * j == target {
                break 'outer (i, j);
            }
            j += 1;
        }
        i += 1;
        if i > 5 {
            break (0, 0);
        }
    };
    println!("pair found: {:?}", result);
}
```

```
pair found: (2, 3)
```

Here two `loop`s are nested. The inner loop has no label, and its own `break` breaks only
itself — when `j` exceeds five, this inner loop ends and the outer loop continues by
incrementing `i`. The outer loop carries the label `'outer`; the line `break 'outer (i, j)`,
right from the middle of the inner loop, breaks the outer loop directly and carries the tuple
`(i, j)` into `result`. When the product `i=2, j=3` gives the target (`6`), this line runs and
the loop chain ends immediately.

This pattern is familiar to any code searching a two-dimensional space: finding the first pair
satisfying a given condition in a table, a grid, or a two-variable search space. If an
unlabeled `break` were used, only the inner loop could be broken when the condition was met;
the outer loop would continue, incrementing `i` and continuing the search — a separate flag
variable to store the result and an extra check in the outer loop would be needed. A labeled
`break` removes this extra checking and makes the search result directly the loop chain's own
value.

**LB16.** A labeled `break` does not only choose which loop is broken, it also determines which
loop a value belongs to. The value of `result` is the value of the outermost `'outer loop` —
the inner loop itself never produced a value, and cannot, because the inner `break;` carries no
value at all. The ability to return a tuple belongs only to the labeled loop; the inner loop
only controls its own flow, it cannot directly touch the outer loop's result — that is possible
only with a labeled `break`.

This is why the two `break` lines in the source do different jobs even though they carry the
same keyword. The unlabeled `break;` breaks the nearest enclosing loop — here, the inner loop —
and carries no value, because the inner loop was never bound to a result. The labeled
`break 'outer (i, j)`, in turn, jumps directly to the named loop no matter how many loops lie
in between, and determines that loop's value. These two uses can stand side by side in the same
lesson, in the same file, because the label only changes the **target**; the syntax and keyword
stay the same.

Labeling applies not only to `break`, but also to `continue` — moving to the next turn of a
labeled loop uses the same `'name:` syntax too. This lesson measured only the value-carrying
`break`, because the course's axis is value production; labeling `continue` follows the same
targeting logic, it just carries no value.

## Limiting Measurement: An if Without else Is Also an Expression

Every `if` seen up to this point carried an `else` branch. An `if` with no `else` branch is
also an expression — only its value is always `()`.

```rust
// if_without_else.rs — an if without else is also an expression, its value is ()
fn main() {
    let number = 4;
    let result = if number % 2 == 0 {
        println!("even found: {}", number);
    };
    println!("the value of an if without else: {:?}", result);
}
```

```
even found: 4
the value of an if without else: ()
```

`result` was genuinely bound to a binding — this proves `if` was evaluated as an expression,
because Rust does not let you bind a statement to a binding. But `result`'s value is `()`,
because what happens when the condition is not met was never written, and Rust fills this gap
with `()` — as if there were an invisible `else { }`.

This is the else-less counterpart of the rule that `if`'s branches must "give the same type":
if the body of an `else`-less `if` tried to produce an `i32` like `number`
(`if number % 2 == 0 { number }`, with no `else` written at all), the compiler would reject it.
Because the unwritten `else` branch is implicitly counted as producing `()`, and if the body
produces `i32`, the two "branches" (the written body, the implicit `else`) would be different
types — just as the two branches in the `if_result` example in the previous section had to be
the same type. If the body of an `else`-less `if` is going to produce a value, that value can
be nothing other than `()`; otherwise a semicolon must be added to the end of the body, silently
discarding the value.

This boundary gives a shortcut when writing source: if an `if` is written only for a side
effect (a print, an update) and there is nothing to do in the opposite case, there is no need
to write an `else` branch — the compiler already expects `()`, and the body already produces
`()`. `else` becomes mandatory only when both branches need to produce a **different** value.

This is an observation from the same family as the previous lesson's semicolon rule. There,
adding a semicolon to a function body's last line turned the value the body produced into
`()`; here, never writing an `if`'s `else` branch reaches the same result by a different path —
the unwritten branch is implicitly counted as producing `()`. Both carry the same principle:
in Rust, a "missing" value is never left undefined anywhere in the source; the missing place
always corresponds to `()`.

The five forms splitting into three also offers a choice when writing source: do you need a
structure that selects a computation among a fixed number of branches (`if`, `match`), or an
open-ended repetition whose stopping condition is not known in advance (`loop`), or a
repetition whose end is known in advance from a condition (`while`, `for`)? These three
questions are independent of one another, and each one's answer brings with it whether that
form will produce a value. Before a programmer asks "will this loop give me a result," it is
enough to look at which loop form was chosen — the answer is hidden in the form itself.

This lesson brings together, in a single place, all the pieces this six-lesson section has
gathered. The first lesson defined the three classes (expression, statement, item) and put them
into numbers with a measurement core; the second lesson showed these classes hold at the
project level too; the third lesson established binding; the fourth lesson established the
types of bound values; the fifth lesson showed how a function body gathers these three classes
into a single return value. This lesson, finally, measured how control flow itself — the five
forms determining which line of a program runs when — fits the same three classes. Nothing
written in the source fell outside this classification: from the smallest operator to the
largest control structure, every line either produces a value, produces `()`, or only declares
a name. This also explains why the single question asked at the start of these six lessons —
does what you write in the source produce a value — did not stay confined to a single lesson
but could carry an entire section: the question does not grow, only the writing it applies to
widens.

## Summary

- Three of the five control-flow forms can produce a value: `if` and `match` always (by
  selecting a branch), `loop` only when a value is given to `break`.
- `while` and `for` never produce a value; both give `()` in every case.
- All of `if`'s and `match`'s branches must give a value of the same type; otherwise the
  compiler rejects it.
- A labeled `break` can carry a value directly out of nested loops to the outermost labeled
  loop; unlabeled loops can only control their own flow.
- An `if` without `else` is also an expression, and its value is always `()`; it does not
  compile if its body tries to produce a different type.
- Nothing written in the source falls outside the expression / statement / item
  classification; this lesson showed that even control flow's most complex forms fall into one
  of the same three answers.

## Next Step

Across these six lessons, whether control flow produces a value was measured by looking at each
form one by one. The next topic carries the same question into **data**: how a type is built,
how many states it carries, and how those states are taken apart — a new section starting with
structs and enums will take up these questions one by one. The types seen so far were the
ready-made types the language itself provides (integer, floating-point, tuple); the next
section takes up how new types are **defined** from these — fields gathered together, states
counted together.
