---
title: 'Unwrapping Optionals'
source: 'https://academia.sh/en/courses/swift/unwrapping-optionals'
course: 'The Swift Language'
language: en
updated: '2026-10-21T05:03:13+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that the right-hand side of if let and guard let is evaluated once and immediately'
    - 'Shows that a ?? chain stops at the first filled candidate and never evaluates the next one'
    - 'Measures that optional chaining on an empty receiver does not evaluate the argument either'
    - 'Explains why force unwrapping moves an absence check out of its place'
---

# Unwrapping Optionals

Measuring when the right-hand side of if let and guard let, a ?? chain and optional chaining are evaluated.

The previous lesson worked without opening the box: `map` and `flatMap` never ran the transformer on an empty box and ran it on every call on a filled one, and the result was a box again. Somewhere in a program a real value is needed: a number to print, the name of a file, a term in a sum. This lesson lists the ways of opening the box and asks the same question of each: when is the unwrapped expression evaluated, and when does the code after unwrapping run?

Swift offers several different tools for opening a box, and this lesson measures five. **Optional binding**, that is `if let` and `guard let`, opens the box once and binds its content to a name. The `??` operator puts a fallback value in place of an empty box. **Optional chaining**, written `?.`, makes a call only if the box is filled. Force unwrapping, the postfix operator written with an exclamation mark, turns absence into a runtime error. The fifth, `map` and `flatMap`, was measured in the previous lesson. Each tool evaluates at a different moment, and the difference decides where error handling has to be written.

The course's measuring axis does not change here either: **when is this expression evaluated?** There are four moments: immediately, on first use, on every use, never. The `Probe` class, which counts by the rule, is given below exactly as it is.

## The Core

Every lesson uses the same core. `Probe` counts how many times an expression is evaluated (`eval`) and how many times it is used (`use`); the `moment` property derives the moment from those two numbers. In the binding measurement of the opening lesson, the distribution over eight expressions was 2/1/3/2: two immediately, one on first use, three on every use, two never. In this lesson we measure only our own slice.

```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 measurements need a small class whose name can be changed. The class is not a measurement itself, it only supplies material for the chaining example, so it has no output.

```swift
// User.swift — a small class whose name can be changed
final class User {
    var name: String
    var renames = 0

    init(name: String) {
        self.name = name
    }

    func rename(_ newName: String) {
        name = newName
        renames += 1
    }
}
```

## Optional Binding

In `if let number = lookup("12", probe: source)`, the right-hand side of the equals sign is an optional. Swift evaluates it once, checks the result, binds the content to `number` if the box is filled, and runs the body of the block. If the box is empty, the body never runs. In the measurement below the right-hand side is wrapped in `source.eval`, and the bound name is used twice in the body.

```swift
// main.swift — if let evaluates its right side once, the bound name is used twice
let source = Probe("if-let-rhs")

func lookup(_ text: String, probe: Probe) -> Int? {
    probe.eval(Int(text))
}

if let number = lookup("12", probe: source) {
    _ = source.use { number + 1 }
    _ = source.use { number * 2 }
}
source.report()
```

```
if-let-rhs: 1 evaluations, 2 uses -> immediately
```

One evaluation, two uses, moment **immediately**. The right-hand side ran before the block was entered; once the name is bound, `number` is an ordinary integer, and using it does not run the right-hand side again. This is a contrast with `map` from the previous lesson: `map` ran the transformer on every call, whereas optional binding runs the expression once and keeps the result in the name. If we want to read the same right-hand side twice later, the bound name needs no extra evaluation.

The bound name is valid only inside the block; when the block ends, `number` does not exist. Writing the same function call again inside a second `if let` means a second evaluation, because `if let` runs its right-hand side at the moment of binding and shares the result with no other `if let`. When a value is needed in several places, the call has to be written once and its result kept in a name.

**TS5.** This measurement is for an `if let` whose right-hand side is a single function call. The form that joins several conditions with commas (`if let a, let b`) and the `where` condition were not measured.

## guard let and the Short Form

`guard let` does the same binding from the opposite direction. If the box is empty, the `else` block runs, and that block must leave the scope (`return`, `throw`, `break`, `continue`, or a call that never returns such as `fatalError`); if the box is filled, the name is bound and stays valid after the `guard` line, for the rest of the function. That is why checks that need an early exit are written with `guard`: the real work stays unindented and the error case is collected at the top. The short form, `if let maybe`, binds under the same name without writing it again.

```swift
// main.swift — guard let and the short if let
func label(for text: String) -> String {
    guard let number = Int(text) else {
        return "invalid"
    }
    return "number \(number)"
}

let maybe: Int? = Int("5")
if let maybe {
    print("short form", maybe)
}
print(label(for: "30"))
print(label(for: "thirty"))
```

```
short form 5
number 30
invalid
```

The first line shows the short form: the optional named `maybe` is used inside the block under the same name, as a non-optional integer. The second and third lines show the two paths of `label(for:)`, which contains the `guard let`: `"30"` passes the binding and the string at the end of the function is built, while `"thirty"` returns from the `else` branch. The bound `number` is an `Int` after the `guard` line; there is no need to open the box again.

The difference between the two binding forms lies in how the weight is distributed, not in a measurable difference in evaluation. `if let` keeps the success path in a block, `guard let` keeps the failure path in a block. Choosing the form that leaves the longer path unindented gives the more readable result.

## The Fallback Value: ??

The `??` operator has two sides. If the optional on the left is filled, its content is the result; if it is empty, the fallback on the right is the result. The expression on the right is a parameter written as an **autoclosure**: Swift wraps it in a closure, and the operator calls it only when needed. This is another example of the transformer from the previous lesson: an expression written in the source may never run until a method calls it.

When written as a chain, this behavior goes one step further. Below there are three candidates: an empty box, a filled box marked with the `second` probe, and a fallback number marked with the `third` probe. The chain stops at the first filled candidate. The loop runs two rounds, and each round uses the result twice.

```swift
// main.swift — a ?? chain stops at the first filled value
let second = Probe("second-candidate")
let third = Probe("third-candidate")
let nothing: Int? = nil
let found: Int? = 8

for _ in 1...2 {
    let result = nothing ?? second.eval(found) ?? third.eval(0)
    _ = second.use { result }
    _ = third.use { result }
}
second.report()
third.report()
```

```
second-candidate: 2 evaluations, 2 uses -> on-every-use
third-candidate: 0 evaluations, 2 uses -> never
```

The second candidate was evaluated in every round: two rounds, two evaluations, moment **on every use**. The third candidate was never evaluated, because the second was filled and the chain was cut there. So when `a ?? b ?? c` is written, `c` is computed only if both `a` and `b` are empty. Putting an expensive fallback on the right therefore creates no wasted work: as long as an earlier candidate is filled, that expression is never evaluated.

This behavior has a side consequence. A side effect in the expression on the right, such as a counter increment or a log line, happens only when the box on the left is empty. Code that wants the side effect to happen in every case should write it before the `??` expression, not inside it.

**TS6.** This measurement is for a chain of three candidates with a filled second candidate fixed. The two-candidate form with a filled left box and the case where the fallback is a function call were not measured separately.

## Optional Chaining

The `?` in `presentUser?.rename("y")` is a question mark and it marks an absence check. If the receiver is filled, the call is made; if the receiver is empty, the call is not made and the value of the expression is `nil`. If the call returned `Int`, chaining would turn it into `Int?`; so the result of chaining is always an optional, and even a call with no return type gives `Void?`.

The real question is the call's argument. When `absentUser?.rename(skipped.eval("x"))` is written, the receiver is empty and the call is not made. The argument expression is something computed before the call; so is it computed when the receiver is empty? We measure with two receivers, one empty and one filled. For each, the argument is a probe marked with `eval`, and the loop runs two rounds.

```swift
// main.swift — when the chain stops at nil, the argument is not evaluated either
let skipped = Probe("nil-receiver")
let reached = Probe("filled-receiver")
let absentUser: User? = nil
let presentUser: User? = User(name: "ada")

for _ in 1...2 {
    absentUser?.rename(skipped.eval("x"))
    presentUser?.rename(reached.eval("y"))
    _ = skipped.use { absentUser?.renames }
    _ = reached.use { presentUser?.renames }
}
skipped.report()
reached.report()
print(presentUser?.name ?? "none", presentUser?.renames ?? -1)
```

```
nil-receiver: 0 evaluations, 2 uses -> never
filled-receiver: 2 evaluations, 2 uses -> on-every-use
y 2
```

With the empty receiver, the argument was never evaluated. The moment chaining sees that the receiver is empty, it skips the rest of the expression, and the argument is part of that rest. With the filled receiver, the argument was evaluated once in every round. The last line confirms the result: the filled user's name became `y`, and two renames were counted over two rounds.

This behavior is expected to hold in longer chains as well: when `a?.b?.c(expensive())` is written and `a` or `b` is empty, `expensive()` must not run; this lesson measured only a chain of two links (TS8). Putting a call with a side effect in an argument to the right of a link that may come back empty makes that side effect conditional.

## Combining the Tools

The five tools are rarely used alone. The last output line above shows two tools together: in `presentUser?.name ?? "none"`, chaining opens the box and reads the name, and because the result is a box again, `??` puts a fallback string in the case where it stays empty. Since the receiver is filled the output was `y`; if the receiver were empty, `none` would have been printed. In the same way, `presentUser?.renames ?? -1` gives the counter value and returns `-1` for an empty receiver.

The evaluation moment of a combination comes from the moments of its parts. Because chaining skips the rest on an empty receiver, `??` looks only once at the box that the chain returns. The fallback sits on the right and is never evaluated as long as the left side is filled. These two rules complement each other: absence is handled in one place only, and after that point the code works with a non-optional value.

**TS7.** This combination is for the case of two tools side by side only, and it was read from the last line of the output. Forms where three or more tools are nested were not measured separately.

## The Bounding Measurement

The rule we built for chaining is this: on an empty receiver the argument is never evaluated. In the previous measurement the receiver was directly a constant and was empty from the start. So what happens if the absence appears in the middle of the chain, as the result of a method call? Below, the `find` method always returns `nil`. One expression calls `find("ada")?.rename(...)` on an empty box; the second element on the same line is not part of the chain but a separate expression.

```swift
// main.swift — a link that comes back empty mid-chain skips the argument of the next call
struct Directory {
    func find(_ name: String) -> User? {
        nil
    }
}

let tail = Probe("middle-empty-next")
let free = Probe("middle-empty-independent")
let directory = Directory()

for _ in 1...2 {
    _ = (directory.find("ada")?.rename(tail.eval("z")), free.eval("w"))
    _ = tail.use { 0 }
    _ = free.use { 0 }
}
tail.report()
free.report()
```

```
middle-empty-next: 0 evaluations, 2 uses -> never
middle-empty-independent: 2 evaluations, 2 uses -> on-every-use
```

Absence born in the middle of the chain also skipped the argument of the next call: when `find` returned empty, the `rename` call was not made and its argument was never evaluated. The independent expression in the second element on the same line, separated by a comma, was evaluated in every round. What is skipped is the chain itself, the part after the `?`; being written on the same line does not make an expression part of the chain.

This result narrows the area where the lesson's rule holds. The sentence "an empty receiver does not evaluate the argument" applies to the expressions that come after the link that stays empty, and in the two links measured it did not depend on where the absence was born. Expressions outside the chain are not within the scope of this rule.

**TS8.** This measurement is for a chain of two links. The behavior of a `?` in the middle of chains of three or more links, and the case where the receiver is a property, were not measured separately.

## Force Unwrapping

Force unwrapping is not measured in this lesson, because there is no evaluation moment to measure: if the box is filled the value inside is taken, and if it is empty the program stops. The fragment below compiles but gives a runtime error when run; since it makes no output claim, it stands here only as source.

```swift
let text = "thirty"
let broken = Int(text)!   // crashes at runtime: the box is empty
print(broken)
```

Force unwrapping moves an absence check from compile time to runtime. The guarantee the compiler gives through the type system is lifted here: a value of type `Int?` is used as an `Int` on the author's word that "this box is filled". If the word is not kept, the program stops. Force unwrapping can be defended where absence is provably impossible, for example in converting a text that is written as a constant in the source and known to be valid; it cannot be defended on data that comes from a user or from outside.

## Summary

- `if let` evaluates its right-hand side once and before entering the block; the bound name causes no further computation.
- `guard let` writes the same binding with an early exit; the bound name is valid after the `guard` line.
- A `??` chain stops at the first filled candidate; a candidate on the right is computed only if the one on the left is empty.
- Optional chaining on an empty receiver skips the call to the right of the `?` and its argument.
- Force unwrapping moves the absence check to runtime and carries no moment to measure.
- This lesson's moment: **immediately**. The right-hand side of `if let` is evaluated once, before the block is entered.

## Next Step

In this lesson the boxes were opened and the values inside were used. The values themselves are defined in one of two ways: as copied structures or as shared classes. The next lesson looks at the difference between structures and classes by measuring at exactly which moment the copy is made.
