Skip to content
Theory · Errors · lesson 1 of 7

Errors and throw

What you'll learn

What you'll learn:

  • declare an error type, throw an error with throw and catch it in do/catch;
  • pass an error on through several functions and catch its separate cases;
  • turn an error into nil with try? and explain when try! crashes the program;
  • clean up on any way out of a function with defer.
A parcel stamped throw rides a belt back from the counter parse to the net catch

Picture a post office where parcels ride a belt from counter to counter. The first counter takes the parcel in, and the last one reads the address and sends the parcel on. But sometimes the address cannot be read. The clerk does not guess and does not toss the parcel into a corner: they stamp it "return to sender", and the belt carries it back. A net is stretched at the first counter, and the parcel lands in it.

The stamp tells the sender at once what went wrong, and they decide what to do next. Functions in a program can do the same. A function that could not do its job hands back an error instead of a result. The error rides back to whoever called the function until someone catches it.

Why is this needed if there are optionals? From the optionals theme you know that Int("42") gives an Int?, and text that is not all digits gives nil. But nil does not say what exactly is wrong:

SWIFT
let empty = Int("")
let letters = Int("4x2")
print(empty == nil, letters == nil) // true true

Both times the result is nil. Yet the troubles differ: in the first case the text is empty, in the second it has a letter. An optional is a box that may be empty, but you cannot put a note with the reason inside. To report the reason, Swift has errors.

The error type and throw

An error in Swift is a value of a type that conforms to the Error protocol. Most often that type is an enum: each of its cases names one reason for a failure. Unlike a compile error, such an error appears while the program is already running. Let's write a function parse that reads a number from text and reports why it could not:

SWIFT
enum ParseError: Error {
    case empty
    case badFormat
}

func parse(_ text: String) throws -> Int {
    if text.isEmpty {
        throw ParseError.empty
    }
    guard let n = Int(text) else {
        throw ParseError.badFormat
    }
    return n
}

Let's go through the program line by line:

  1. enum ParseError: Error declares an enum with two reasons: empty means the text is empty, badFormat means the text is not a number. : Error after the name is the conformance to the Error protocol.
  2. The word throws after the parameters says that the function can throw an error. It stands before the arrow ->.
  3. throw ParseError.empty throws an error if the text is empty.
  4. throw leaves the function like return does, so it also fits the else of a guard.
  5. If both checks pass, return n returns the number as usual.

To throw an error means to end the function at once and, instead of a result, hand an error to whoever called the function. The lines below throw no longer run. If you remove throws from the header of parse, the lines with throw do not compile. The compiler requires you to say in advance that the function can hand back an error.

Interviewers like to ask this: what is Error? The answer:

  • Error is an empty protocol: it has no property or method requirements, so an enum needs only : Error after its name;
  • conforming to Error is what allows a value to be thrown with throw;
  • errors are usually described by an enum, and its cases name the reasons for a failure.

Catching an error: do, try, catch

A function with throws is called differently from an ordinary one. You write try before the call and put the call itself in a do block, followed by catch:

SWIFT
do {
    let n = try parse("42")
    print(n + 1) // 43
    print(try parse("4x2"))
    print("done")
} catch {
    print("error:", error) // error: badFormat
}

Let's walk through the program:

  1. do { … } is a block inside which an error may arrive.
  2. try marks a call that can throw an error. It is also written inside an expression: print(try parse("4x2")).
  3. parse("42") returns 42, and the program prints 43.
  4. parse("4x2") throws ParseError.badFormat. The rest of the do block is skipped: neither the print with this call nor print("done") runs.
  5. Execution moves to catch. Inside it, the caught error is in the constant error, and print shows its case: badFormat.
  6. After catch, the program goes on to the lines below it and does not return to do.

To catch an error means to take it in catch and decide what to do: print a message, put in a value or ask for the text again. A thrown error is caught by the nearest catch on its way back; here it is the one right after do. If there was no error, catch does not run at all.

Here the post office picture is off. A clerk may put a hard parcel aside and come back to it later. A function does not go back to work after throw: the lines below throw will not run, and the call will not return any value.

Common mistake

A function with throws was called without try:

SWIFT
do {
    let k = parse("7") // ✗
    print(k)
} catch {
    print(error)
}
TEXT
error: call can throw but is not marked with 'try'

The program does not compile. The message says: the call can throw an error, but it is not marked with try. The compiler requires try on every such call, so that the code shows where the program may turn off into catch. The fix is let k = try parse("7").

Key points

  • An error is a value of a type that conforms to the Error protocol; usually it is an enum with the reasons for a failure.
  • A function with throws throws an error with throw and ends at once, without a result.
  • A function with throws is called with try inside do: on an error the rest of do is skipped and catch runs, where the error is in the constant error.
  • Error is an empty protocol: for an enum, : Error after its name is enough to conform.