---
title: 'Error Handling'
source: 'https://academia.sh/en/courses/swift/error-handling'
course: 'The Swift Language'
language: en
updated: '2026-10-21T05:03:12+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Define an error type as an `enum` that conforms to `Error`'
    - 'Write a function that throws a specific error type with `throws(ParseError)`'
    - 'Handle an error with `do`/`catch`, `try?` and `rethrows`'
    - 'Measure that statements after a `throw` are not evaluated and that a `defer` block runs on every exit'
---

# Error Handling

Measures that nothing after the `throw` line in the body of a throwing function ever runs, while a `defer` block runs on every exit.

So far every body has run to its end, but reaching the end is not guaranteed. An **error** is the way a function tells its caller that it cannot produce a result. In Swift this is done by throwing a value: the `throw` statement cuts the body at that point and hands control to the code that handles the error. In this lesson we measure what happens to the rest of the cut body.

An error value can be of any type that conforms to the `Error` protocol; in practice it is an `enum`, because error kinds form a finite, nameable set. A **throwing function** carries `throws` in its signature, and the caller has to call it with `try`. The `throws(ParseError)` form writes the single error type that can be thrown into the signature.

## The Measurement Setup

```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)")
    }
}
```

**TS43.** To measure the error case, a `Probe` is passed to the function that is called with broken input; the statement after the `throw` uses that `Probe`. The error is printed as a `case`; its description text is not printed.

## Nothing After a Throw Runs

```swift
// Parsing.swift — the error type and the throwing function
enum ParseError: Error {
    case empty, notNumber, negative
}

func parse(_ text: String) throws(ParseError) -> Int {
    if text.isEmpty { throw .empty }
    guard let value = Int(text) else { throw .notNumber }
    if value < 0 { throw .negative }
    return value
}

func load(_ text: String, afterThrow tracker: Probe) throws(ParseError) -> Int {
    let value = try parse(text)
    return tracker.eval(value)
}
```

The error type, `parse` and `load` sit in a separate file because several measurements use them. The measurement file builds on top of it.

```swift
// main.swift — typed throws, do/catch, try? and rethrows
func apply(_ work: () throws -> Int) rethrows -> Int {
    try work()
}

do {
    _ = try parse("")
} catch {
    print(error)
}

do {
    _ = try parse("7x")
} catch .notNumber {
    print("not a number")
} catch {
    print(error)
}

print((try? parse("-3")) ?? -1)
print((try? apply { try parse("41") }) ?? -1)

let broken = Probe("broken-input")
let healthy = Probe("healthy-input")

_ = broken.use { try? load("abc", afterThrow: broken) }
_ = broken.use { try? load("", afterThrow: broken) }
_ = healthy.use { try? load("12", afterThrow: healthy) }
_ = healthy.use { try? load("13", afterThrow: healthy) }

let all = [broken, healthy]
for probe in all { probe.report() }
tally(all)
```

```
empty
not a number
-1
41
broken-input: 0 evaluations, 2 uses -> never
healthy-input: 2 evaluations, 2 uses -> on-every-use
immediately: 0
on-first-use: 0
on-every-use: 1
never: 1
```

When the `try parse(text)` line throws, `load` ends right there. The `tracker.eval(value)` expression after it never ran in either of the two calls with broken input: **never**. With healthy input every call went to the end and was evaluated again on each call: **on every use**. The error path cuts the body like a short circuit; for the statements that follow there is no "maybe it runs".

`do`/`catch` handles the error: `catch .notNumber` writes a pattern, and the last `catch` takes every remaining error and provides the implicit `error` value. `try?` turns the error into `nil`, so a default value can be supplied with `??`. `rethrows` marks a function that throws if the closure it takes as a parameter throws, and does not throw otherwise; `apply` above behaves according to whether its closure throws. **TS44.** The printed error form is the `case` name; this was measured because the error `enum` carries no associated values, and `case`s with associated values were not tried.

The difference between `throws(ParseError)` and plain `throws` is that the set of errors the caller may meet shows up in the signature. When a function with a written type is called inside a `do throws(ParseError)` block, the `error` value of the `catch` block is directly a `ParseError`, and a pattern such as `catch .notNumber` is matched against the `case` names of the `enum` at compile time. Untyped `throws` tells the caller only that "some `Error` may be thrown". The evaluation rule is the same in both forms: the moment `throw` runs, the body ends, and this is a behavior you can see from the counter of the measurement setup.

The practical consequence of this rule is that the position of a statement with a side effect on the error path matters. A statement that increments a counter, writes a record or changes a resource runs on every path if it stands before the `try` line, and only on the successful path if it stands after. Which one you want is the contract of the function: writing first and validating afterwards gives a different behavior from validating first and writing afterwards.

`try?` does not change this rule, only the appearance of the error: a call that throws returns `nil`, but what follows the throwing point still does not run. This is why the `broken-input` line of the measurement is **never** even though it was called with `try?`. Swallowing the error and the body continuing are two separate things.

## The Question-Mark and the Forced try

There are three short ways to handle an error: `do`/`catch`, `try?` and the forced `try`. **`try?`** turns a throwing call into an optional value; the error becomes `nil` and a success becomes the wrapped value. The **forced `try`** (the exclamation-mark form) is a claim that "this call never throws"; if it throws, the program crashes. Neither changes the call itself, they only choose where the error goes. So they are expected to look alike in terms of the moment of evaluation. The block below measures both with input that does not throw; calling the forced `try` with throwing input would crash the program, so this lesson does not measure that.

```swift
// main.swift — try? and the forced try, with non-throwing input
let optionalProbe = Probe("try-question")
let forcedProbe = Probe("try-bang")

let first = optionalProbe.use { try? load("21", afterThrow: optionalProbe) }
let second = optionalProbe.use { try? load("22", afterThrow: optionalProbe) }
let third = forcedProbe.use { try! load("23", afterThrow: forcedProbe) }
let fourth = forcedProbe.use { try! load("24", afterThrow: forcedProbe) }

print(first ?? -1, second ?? -1, third, fourth)

let all = [optionalProbe, forcedProbe]
for probe in all { probe.report() }
```

```
21 22 23 24
try-question: 2 evaluations, 2 uses -> on-every-use
try-bang: 2 evaluations, 2 uses -> on-every-use
```

Both forms ran the body on every call: **on every use**. Neither `try?` nor the forced `try` brings a cache or a deferral; both execute the call right there, immediately. The difference is in the shape of the result: `try?` gives an `Int?` that may be empty, the forced form gives a plain `Int` that cannot be empty. The behavior of the error path is the side that is not measured here. With `try?` a `nil` comes back and the program goes on, with the forced form the program stops. **TS45.** The measurement covers only the path that does not throw; the error path of the forced `try` was deliberately not measured.

## When rethrows Calls the Closure

A function marked `rethrows` may throw if the closure it takes throws, and never throws if the closure does not. For the caller, the visible result is that there is no need to write `try` when the closure does not throw. But when does the body of the closure run? `apply` calls the closure immediately; in the measurement we put two cases side by side: a closure that does not throw, and a closure that throws, but calls `parse` before it gets to the point of running its own statement.

```swift
// main.swift — rethrows with a throwing and a non-throwing closure
func apply(_ work: () throws -> Int) rethrows -> Int {
    try work()
}

let plain = Probe("non-throwing-closure")
let throwing = Probe("throwing-closure")

_ = plain.use { apply { plain.eval(5) } }
_ = plain.use { apply { plain.eval(6) } }

_ = throwing.use {
    try? apply {
        let value = try parse("x")
        return throwing.eval(value)
    }
}
_ = throwing.use {
    try? apply {
        let value = try parse("")
        return throwing.eval(value)
    }
}

let all = [plain, throwing]
for probe in all { probe.report() }
```

```
non-throwing-closure: 2 evaluations, 2 uses -> on-every-use
throwing-closure: 0 evaluations, 2 uses -> never
```

The non-throwing closure was called without `try` and ran on every call. In the two calls where the throwing closure was cut at the `parse` line, the `eval` after it was never evaluated. So `rethrows` adds no rule to the body of the closure: the closure runs the moment it is called, and what happens inside it is subject to the `throw` rule of the first section. `rethrows` decides only how the error passes outward, that is, whether the signature requires `try`. **TS46.** The default case, where the closure is passed as a parameter, was measured; a closure stored in a property and called later was not tried.

## Catching a Typed Error Exhaustively

In the introduction we said that the `throws(ParseError)` form carries the set of errors into the signature. The concrete counterpart shows on the catching side: when a `do` block declares the thrown type with `do throws(ParseError)`, the `error` inside `catch` is obtained as a `ParseError`, and a `switch` can cover all the `case`s without a `default`. When a new `case` is added, the compiler points to that `switch`. The function below does this with the `do throws(ParseError)` form and returns a separate recovery value for each `case`.

```swift
// main.swift — an exhaustive switch with a typed error
func recover(_ text: String, handler: Probe) -> Int {
    do throws(ParseError) {
        return try parse(text)
    } catch {
        switch error {
        case .empty: return handler.eval(0)
        case .notNumber: return handler.eval(-1)
        case .negative: return handler.eval(-2)
        }
    }
}

let handler = Probe("catch-body")
let inputs = ["5", "", "q", "-9"]
var values: [Int] = []
for text in inputs {
    values.append(handler.use { recover(text, handler: handler) })
}

print(values)
handler.report()
```

```
[5, 0, -1, -2]
catch-body: 3 evaluations, 4 uses -> on-every-use
```

Three of the four calls had errors, and the `catch` body ran only in those three: four uses and three evaluations. The successful call never reached the `catch`. Because the body was evaluated more than once, the rule counts it as **on every use**; but those evaluations cover not every call, only the calls with errors. The `switch` in the same block has no `default`; this is a consequence of the caught type being known as `ParseError`. With untyped `throws` the same `switch` would not compile, because `error` would be an `any Error` and a `default` would be required. **TS47.** This behavior was measured on Swift 6.4 with the `do throws(ParseError)` form; it was not assumed that the form exists in older versions.

## Turning an Error into a Value: Result

Instead of catching an error right away, you can also carry it as a value. **`Result`** is an enum that stores either a success or an error. The `Result { ... }` form calls the closure immediately and puts the outcome or the error inside. After that you can read the value with `get()` as often as you want. The question here is: does the body run again when a `Result` is read?

```swift
// main.swift — Result turns an error into a value, the body runs once
let resultProbe = Probe("result")

let result = Result<Int, ParseError>(catching: { () throws(ParseError) -> Int in
    try load("8", afterThrow: resultProbe)
})

_ = resultProbe.use { try? result.get() }
_ = resultProbe.use { try? result.get() }

resultProbe.report()
```

```
result: 1 evaluations, 2 uses -> immediately
```

The body ran once, while the `Result` was being built, and the two `get()` calls did not run it again: **immediately**. This is the opposite of executing the call again each time with `try?`. `Result` also makes the error a value, so the throwing point and the handling point can be separated: an error is caught in one place, handled in another, and the body does not run again while it is handled. **TS48.** The `Result` initializer was measured with input that does not throw; it was not claimed by this measurement that the body also runs once with faulty input.

## An Error on an Array: Stopping at the First Error

The standard library's `map` is a `rethrows` function that throws itself if its closure throws. When we transform an array of three elements and the second element gives an error, what happens to the third? We bind a separate `Probe` to each element and call the transformation with `try?`; in the end we can see which elements were evaluated.

```swift
// main.swift — map stops at the first error
let inputs = ["4", "x", "6"]
let probes = [Probe("first"), Probe("second"), Probe("third")]

let converted = try? inputs.indices.map { index -> Int in
    try load(inputs[index], afterThrow: probes[index])
}

print(converted == nil)
for probe in probes { probe.report() }
```

```
true
first: 1 evaluations, 0 uses -> immediately
second: 0 evaluations, 0 uses -> never
third: 0 evaluations, 0 uses -> never
```

The first element was transformed, the second element gave an error and the third was never reached. `use` was not called in this measurement; the question is how many elements were evaluated, which is why the use count is zero and the first one's **immediately** label shows only the single evaluation. A `map` that moves along the array is cut at the first error; for the following elements there is no behavior of "try anyway". This is the `throw` rule of a single function body reflected at the array level: what is cut is not the rest of a body but the remaining turns of the loop. If you want to try all the elements and collect the errors, you have to turn each element into its own `Result`. **TS49.** The measurement was made with `indices.map`; it was not assumed that other collection types or `lazy` sequences stop in the same way.

## The Bounding Measurement

The rule that nothing after a throw runs has one exception: a **`defer`** block runs even when the scope is left with `return` or `throw`. If the program ends with `fatalError` or `exit`, `defer` does not run. Both the broken-input path and the successful path run the `defer`.

```swift
// main.swift — defer runs on every exit
func guarded(_ text: String, cleanup: Probe) throws(ParseError) -> Int {
    defer { _ = cleanup.eval(0) }
    return try parse(text)
}

let cleanup = Probe("defer-cleanup")
_ = cleanup.use { try? guarded("12", cleanup: cleanup) }
_ = cleanup.use { try? guarded("x", cleanup: cleanup) }
cleanup.report()
```

```
defer-cleanup: 2 evaluations, 2 uses -> on-every-use
```

In both of the two calls, one successful and one with an error, the `defer` ran: **on every use**. This has to be read together with the previous measurement. On the same error path, the ordinary statement after the `throw` stayed **never**; the `defer` ran. Work that has to be done on every exit path, such as releasing resources or rolling back a state, you write in a `defer`, not in an ordinary statement. **TS50.** The `defer` measurement was made in a function with a single exit point; its ordering across several `return` paths was not measured.

## Summary

- An error type is an `enum` that conforms to `Error`; `throws(ParseError)` writes the type that can be thrown into the signature.
- `do`/`catch` handles with a pattern, `try?` turns the error into `nil`, and `rethrows` passes on the error of a closure.
- `throw` cuts the body at that point; the statements after it are never evaluated.
- A `defer` block runs the same way on a successful and on an erroneous exit.
- This lesson's moment: **never** (the statement after the `throw` with broken input).

## Next Step

Where a property or a function can be seen from is also a design decision. The next lesson measures access control and how a `private` cache looks from the outside.
