---
title: 'Control Flow'
source: 'https://academia.sh/en/courses/swift/control-flow'
course: 'The Swift Language'
language: en
updated: '2026-10-21T05:03:11+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Use the `if` / `else if` chain and pattern matching inside `switch`'
    - 'Show that the conditions after the first match are not evaluated'
    - 'Read how many times `for ... where` and `while` conditions are evaluated'
    - 'Explain the exhaustiveness requirement of the `switch` statement'
---

# Control Flow

Introduces conditions, loops and the pattern-matching `switch` statement; measures that `where` conditions after the first match are never evaluated.

Operators joined two expressions, and the right-hand side was never evaluated when it was not needed. Control flow carries the same idea one step up: conditions decide which branch runs, and a construct that has found the first suitable branch does not look at the rest. The question of this lesson is this: in a chain, when are the conditions that come after a match evaluated?

## Conditions, Loops and switch

`if` asks a `Bool` condition; an `else if` chain tries the conditions from top to bottom. `for n in 1...3` runs the body for every value in a range, and `while` goes on as long as the condition is true. `switch` compares a value with patterns. A `case` can be a value, a range, a tuple or a condition decorated with `where`. In Swift the `case` bodies do not fall through to the next one; the first match runs and the `switch` ends.

A `switch` has to be exhaustive: every possible value must be covered by a `case`, or there must be a `default`. The snippet below handles only 1 and 2 for an `Int`. This snippet does not compile in Swift 6.4; the compiler rejects it, which is why it has no output.

```swift
let n = 3
switch n {
case 1: print("one")
case 2: print("two")
}
```

The error is a real guarantee: a `switch` that does not cover a newly added value does not compile. The fix is to add a `default` or to catch the remaining values with a pattern.

```swift
// Probe.swift — records when an expression is evaluated
final class Probe {
    let label: String
    private(set) var evaluations = 0
    private(set) var uses = 0
    private var evaluatedBeforeUse = false

    init(_ label: String) {
        self.label = label
    }

    func eval<T>(_ value: T) -> T {
        evaluations += 1
        if uses == 0 { evaluatedBeforeUse = true }
        return value
    }

    func use<T>(_ access: () -> T) -> T {
        uses += 1
        return access()
    }

    var moment: String {
        if evaluations == 0 { return "never" }
        if evaluations > 1 { return "on-every-use" }
        return evaluatedBeforeUse ? "immediately" : "on-first-use"
    }

    func report() {
        print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
    }
}

func tally(_ probes: [Probe]) {
    for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
        let count = probes.filter { $0.moment == moment }.count
        print("\(moment): \(count)")
    }
}
```

The measurement block builds two chains. In the first, the `grade` function evaluates a score of 75: the first condition is false, the second is true and the third is never reached. In the second, the `describe` function tests the value `-5` with a `switch`: the `where` condition of the first `case` is true, and the `where` condition of the next `case` is never asked.

```swift
// main.swift — what follows the first match in a chain
func grade(_ score: Int, _ second: Probe, _ third: Probe) -> String {
    if score >= 90 {
        return "A"
    } else if second.eval(score >= 70) {
        return "B"
    } else if third.eval(score >= 50) {
        return "C"
    }
    return "F"
}

func describe(_ n: Int, _ first: Probe, _ later: Probe) -> String {
    switch n {
    case let x where first.eval(x < 0):
        return "negative"
    case let x where later.eval(x > 100):
        return "large"
    default:
        return "other"
    }
}

for n in 1...3 { print("step", n) }

let secondProbe = Probe("else-if-second-condition")
let thirdProbe = Probe("else-if-third-condition")
let firstWhereProbe = Probe("switch-first-where")
let laterWhereProbe = Probe("switch-later-where")

for _ in 1...2 {
    let letter = secondProbe.use { thirdProbe.use { grade(75, secondProbe, thirdProbe) } }
    print(letter)
    let kind = firstWhereProbe.use { laterWhereProbe.use { describe(-5, firstWhereProbe, laterWhereProbe) } }
    print(kind)
}

let allProbes = [secondProbe, thirdProbe, firstWhereProbe, laterWhereProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
```

```
step 1
step 2
step 3
B
negative
B
negative
else-if-second-condition: 2 evaluations, 2 uses -> on-every-use
else-if-third-condition: 0 evaluations, 2 uses -> never
switch-first-where: 2 evaluations, 2 uses -> on-every-use
switch-later-where: 0 evaluations, 2 uses -> never
immediately: 0
on-first-use: 0
on-every-use: 2
never: 2
```

`else-if-second-condition` was evaluated on every call, because the first condition was false and so the chain came to it. `else-if-third-condition`, on the other hand, came out `never` in both calls: the second condition was already true and the chain ended there. The same holds for `switch`: the first `where` was tested every time, the later `where` was never tested. So a side effect inside a `where` condition never happens if an earlier `case` matched.

**LB39.** The measurement was done with a single input value (75 and -5); for other inputs, which condition stays `never` changes.

**LB40.** That `switch` is tried from top to bottom in source order was concluded from the measurement; whether the compiler changes the order was not measured separately.

## Early Exit with guard

`guard` verifies a condition and forces an exit from the scope if the condition does not hold. The syntax is `guard condition else { exit }`; the `else` block has to end with `return`, `break`, `continue` or a call that stops the program. If the condition holds, control continues after the `guard` line, and the names bound in the condition are visible in the rest of that scope. It does the same job as `if`, but turns the intent around: the unwanted case is ruled out first, then the real work is written on a flat path.

The block below writes a function that accepts a positive number. The valid path writes to one `Probe`, the invalid path to another. The function is called with three numbers, and before each call the two `Probe` objects each record a use.

```swift
// Guard.swift — early exit with guard
func check(_ number: Int, valid: Probe, invalid: Probe) -> String {
    guard number > 0 else { return invalid.eval("invalid") }
    return valid.eval("valid")
}
func observe<T>(_ watched: [Probe], _ work: () -> T) -> T {
    for probe in watched { _ = probe.use { 0 } }
    return work()
}
let validProbe = Probe("valid-path")
let invalidProbe = Probe("invalid-path")
for number in [5, -2, 7] {
    print(observe([validProbe, invalidProbe]) {
        check(number, valid: validProbe, invalid: invalidProbe)
    })
}
validProbe.report()
invalidProbe.report()
```

```
valid
invalid
valid
valid-path: 2 evaluations, 3 uses -> on-every-use
invalid-path: 1 evaluations, 3 uses -> on-first-use
```

The three numbers gave the results `valid`, `invalid`, `valid`. The valid path ran twice and the invalid path once: `valid-path` came out `on-every-use` with two evaluations and three uses, and `invalid-path` came out `on-first-use` with one evaluation and three uses. The use count came out three, because `observe` recorded a use before each call. The expression a path produces is evaluated only when that path is chosen; the expression of the path that is not chosen never runs.

`guard` makes it readable which path runs at which moment: the exceptional path stands at the very start, and the remaining lines run only when the condition holds. The measurement confirms this reading.

**LB41.** `guard` was measured only with an integer condition; using `guard let` with optional values is the job of a later topic.

## switch Patterns

The `switch` statement compares a value with patterns in order. Patterns do not have to be only constants: tuple patterns compare several values at once, `let` binds a value, and `where` adds a condition. For example, `case (let x, 0)` catches every tuple whose second element is zero and binds the first element to the name `x`. The first matching case runs and the others are never evaluated.

The block below writes a function that finds the class of a point. Each of the three cases writes to its own `Probe`. The function is called with three points: the origin twice, a point on the horizontal axis once; the third case never matches.

```swift
// Pattern.swift — switch patterns
func classify(_ x: Int, _ y: Int, watched: [Probe]) -> String {
    switch (x, y) {
    case (0, 0):
        return watched[0].eval("origin")
    case (let along, 0):
        return watched[1].eval("horizontal \(along)")
    case (_, let height) where height > 5:
        return watched[2].eval("high \(height)")
    default:
        return "other"
    }
}
func observe<T>(_ watched: [Probe], _ work: () -> T) -> T {
    for probe in watched { _ = probe.use { 0 } }
    return work()
}
let originProbe = Probe("origin-case")
let axisProbe = Probe("axis-case")
let highProbe = Probe("high-case")
let cases = [originProbe, axisProbe, highProbe]
for point in [(0, 0), (4, 0), (0, 0)] {
    print(observe(cases) { classify(point.0, point.1, watched: cases) })
}
for probe in cases { probe.report() }
```

```
origin
horizontal 4
origin
origin-case: 2 evaluations, 3 uses -> on-every-use
axis-case: 1 evaluations, 3 uses -> on-first-use
high-case: 0 evaluations, 3 uses -> never
```

The three calls gave the results `origin`, `horizontal 4`, `origin`. `origin-case` came out `on-every-use` with two evaluations, `axis-case` `on-first-use` with a single evaluation, and `high-case` `never` with zero evaluations. The third case was never tried despite its `where` condition, because the first two cases had already caught the points. So `switch` looks in order, takes the first match and does not touch the rest.

This rule matters when you order patterns: if you write a general case before a specific case, the specific case never runs. The compiler rejects missing coverage as an error, but in some cases it cannot find a wrong order.

**LB42.** Patterns were measured with tuples and `where`; enumeration cases and range patterns are outside this lesson.

## Labeled Statements

In nested loops `break` ends only the innermost loop. To end the outer loop you give the loop a label: `outer: for row in 1...3 { ... }`. A `break outer` inside jumps to this label and the outer loop ends. `continue` with the same label moves to the next iteration of the outer loop. The label is the way to write explicitly which loop you mean.

The block below writes a function that looks for a particular product in a multiplication table. Each product is computed through a `Probe`; so the number of iterations run is measured. The function is called with two targets.

```swift
// Label.swift — search with a labeled break
func search(target: Int, probe: Probe) -> String {
    var found = "none"
    outer: for row in 1...3 {
        for col in 1...3 {
            if probe.eval(row * col) == target {
                found = "\(row)x\(col)"
                break outer
            }
        }
    }
    return found
}
let searchProbe = Probe("product-search")
print(searchProbe.use { search(target: 4, probe: searchProbe) })
print(searchProbe.use { search(target: 6, probe: searchProbe) })
searchProbe.report()
```

```
2x2
2x3
product-search: 11 evaluations, 2 uses -> on-every-use
```

The first search found `2x2`, the second `2x3`. Eleven evaluations were needed in total: five products were computed in the first search (1, 2, 3, 2, 4), and six in the second (1, 2, 3, 2, 4, 6). `break outer` ended both loops at once as soon as the product was found, so the remaining products were never computed. An unlabeled `break` would have ended only the inner loop, and the outer loop would have continued with the next row. The moment reads as `on-every-use`.

The labeled exit protects the search from unnecessary products. The label also shows how many loops the code skips; the reader knows from its name which loop ends.

**LB43.** The labeled `break` was measured with two nested loops; using a label with `continue` is outside this lesson.

## At Least One Round with repeat-while

A `while` loop checks the condition first; if the condition is false from the start, the body never runs. `repeat-while` turns this around: the body runs first and the condition is checked afterwards. So the body runs at least once, whatever the condition is. When you need to read a value at least once, or to do an action once and then decide whether to go on, this form is the right tool.

The block below runs two loops with the same condition that is false from the start. Each body increases a counter by a one that comes from a `Probe`. One is written with `while`, the other with `repeat-while`.

```swift
// Repeat.swift — repeat-while runs at least one round
let whileProbe = Probe("while-body")
let repeatProbe = Probe("repeat-body")
var whileCount = 0
var repeatCount = 0
_ = whileProbe.use {
    while whileCount < 0 { whileCount += whileProbe.eval(1) }
}
_ = repeatProbe.use {
    repeat { repeatCount += repeatProbe.eval(1) } while repeatCount < 0
}
print(whileCount, repeatCount)
whileProbe.report()
repeatProbe.report()
```

```
0 1
while-body: 0 evaluations, 1 uses -> never
repeat-body: 1 evaluations, 1 uses -> on-first-use
```

The two loops with the same false-from-the-start condition gave `0 1`: `while` never increased the counter, `repeat-while` increased it once. `while-body` came out `never` with zero evaluations, and `repeat-body` `on-first-use` with one evaluation. In a `repeat-while` loop the condition is checked after the body, so the expression of the body ran once even though the condition was false.

The question of choice is this: does the body have to run at least once before the condition? If you read a value and decide afterwards, `repeat-while` is the right tool; if you want to do nothing when the condition does not hold from the start, `while` is.

**LB44.** `repeat-while` was measured with a condition that is false from the start; that the two forms give the same result in loops where the condition stays true is outside this lesson.

## The Limiting Measurement

The conditions in a chain were asked once and dropped. In loops the condition is asked again on every round. `for ... where` evaluates the condition on every iteration; so the expression inside `where` is `on-every-use` in this line. `while` carries a subtle detail: the condition is evaluated one more time when leaving the loop, because the only way out of the loop is the condition returning `false`. A `while` that lasts three rounds therefore evaluates the condition four times.

```swift
// main.swift — loop conditions
let whereProbe = Probe("for-where-condition")
let whileProbe = Probe("while-condition")

for _ in 1...2 {
    var evens: [Int] = []
    whereProbe.use {
        for n in 1...4 where whereProbe.eval(n % 2 == 0) { evens.append(n) }
    }
    print(evens)
    whileProbe.use {
        var i = 0
        while whileProbe.eval(i < 3) { i += 1 }
    }
}

let allProbes = [whereProbe, whileProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
```

```
[2, 4]
[2, 4]
for-where-condition: 8 evaluations, 2 uses -> on-every-use
while-condition: 8 evaluations, 2 uses -> on-every-use
immediately: 0
on-first-use: 0
on-every-use: 2
never: 0
```

Over the two rounds, the `for ... where` condition was evaluated eight times, once for each of four iterations per round. The `while` condition got eight evaluations although it made three iterations in each of the two rounds: the number of iterations plus one. The extra evaluation is the `false` query that ends the loop.

**LB45.** The "iterations plus one" result for `while` was measured in this three-iteration example; a loop that exits with `break` or `return` has no extra query, but this was not measured in this lesson.

## Summary

- The `if` / `else if` chain tries the conditions from top to bottom and stops at the first true one.
- A `switch` has to be exhaustive; a missing `case` is rejected by the compiler.
- The `where` conditions and `else if` conditions after the first match are never evaluated.
- The `for ... where` condition is evaluated on every iteration, and the `while` condition one more time than the number of iterations.
- The moment of this lesson: **never** (the `where` after a match).

## Next Step

A condition only tells whether a branch runs. There are also expressions whose evaluation moment during a call is the question: the default argument of a function. The next lesson measures argument labels and default values.
