Skip to content
Theory · Errors · lesson 3 of 7

catch and passing an error on

In the first lesson, parse was called right from the do block. In a real program, another function often stands between them.

Back to the post office: there are three counters on the belt. The first one takes parcels in, and a catch net is stretched at it. The second one works out the postage. The third one, parse, reads the weight written on the parcel.

If the weight cannot be read, parse stamps the parcel throw, and it rides back. The second counter does not catch it but lets it pass, and it lands in the first counter's net. Let's start with the enum and the parse function we have already met:

SWIFT
enum ParseError: Error {
    case empty, badFormat
}

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

parse has three outcomes:

  • empty text gives the error empty;
  • text that is not all digits gives the error badFormat;
  • text with a number gives the number itself.

An error passes through a function

Let's add the second counter: the function cost. It gets the weight as text, reads it with parse and works out the price: three coins per kilogram.

SWIFT
func cost(_ weight: String) throws -> Int {
    let kg = try parse(weight)
    return kg * 3
}

do {
    print(try cost("4")) // 12
    print(try cost(""))
} catch {
    print("error:", error) // error: empty
}

cost has no do/catch of its own. It does not catch the error from parse but hands it on to whoever called cost itself. This is called passing the error on. Passing an error on takes two things:

  • try before the call to parse: the place an error may arrive from;
  • throws in the header of cost: a warning to its callers that an error may come out of this function too.

Press "Next step" and watch the column of frames: on the second call, the error removes the frames of parse and cost, and neither of them returns a number.

LiveAn error leaves two callsstep 1 of 10 · frames: 1

The program calls cost with the text 4 and waits for the price.

Watch the frames: the error removes parse and cost, and neither returns a number

What the widget shows:

  • throw ends parse at once, without return;
  • in cost the error leaves at the line with try, and return kg * 3 no longer runs;
  • the error is caught by the nearest catch on the way back, however many functions stand between.

Interviewers like to ask this: how does an error pass through several functions? Each function on the error's path does one of two things:

  • catches the error with its own do/catch;
  • passes it on: then the function is marked throws, and the call inside it is marked try.

Catching separate cases

Right now catch catches any error. But different reasons often need different answers: for empty text, ask for the weight; for letters, say that it is not a number. For this, do can have several catch branches:

SWIFT
do {
    print(try cost("4x"))
} catch ParseError.empty {
    print("no weight")
} catch {
    print("other:", error) // other: badFormat
}

The catch branches are checked from top to bottom, like the branches of a switch, and the first one that fits runs.

A branch for one case

catch ParseError.empty catches only this case of the error. Here cost("4x") threw badFormat, so the branch did not fit, and no weight was not printed.

A branch for the rest

A catch without a pattern catches everything the branches above did not. Inside it, the caught error is in the constant error, and print shows its case. Such a branch usually goes last.

If nobody catches the error

In the main part of the program, that is, in the lines of main.swift outside any function, a call with try can also be written without do: print(try cost("")). The program compiles, but there is nobody to catch the error, and at run time it crashes:

TEXT
Fatal error: Error raised at top level: main.ParseError.empty

The message says: the error reached the main part of the program. The error itself is named at the end: the type ParseError and the case empty. main before them is the program's name: the command swiftc -swift-version 6 main.swift builds a program called main. If the program is called something else, in Xcode for example, its name stands in place of main.

Here the post office picture is off. A parcel nobody collected goes to the storeroom, and the post office waits. An error nobody caught stops the whole program.

Common mistake

A function calls parse with try, but throws was left out of its header:

SWIFT
func price(_ weight: String) -> Int { // ✗
    let kg = try parse(weight)
    return kg * 3
}
TEXT
error: errors thrown from here are not handled

The program does not compile. The message says: nothing handles the error that may arrive from here. A function without throws has two ways out:

  • add throws to the header and pass the error on;
  • catch the error inside the function with its own do/catch, ending it with a catch without a pattern.

Key points

  • A function with throws may pass an error on instead of catching it: the call inside it is written with try, and the error goes to the caller.
  • An error removes every call on its path until the nearest catch catches it; the lines below try do not run.
  • catch ParseError.empty catches one case, and a catch without a pattern catches all the others, with the error in the constant error.
  • An error nobody caught crashes the program: Fatal error: Error raised at top level.