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:
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.
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:
trybefore the call toparse: the place an error may arrive from;throwsin the header ofcost: 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.
1enum ParseError: Error {2 case empty, badFormat3}4 5func parse(_ text: String) throws -> Int {6 if text.isEmpty {7 throw ParseError.empty8 }9 guard let n = Int(text) else {10 throw ParseError.badFormat11 }12 return n13}14 15func cost(_ weight: String) throws -> Int {16 let kg = try parse(weight)17 return kg * 318}19 20do {21 print(try cost("4"))22 print(try cost(""))23} catch {24 print("error:", error)25}
The program calls cost with the text 4 and waits for the price.
What the widget shows:
throwendsparseat once, withoutreturn;- in
costthe error leaves at the line withtry, andreturn kg * 3no longer runs; - the error is caught by the nearest
catchon 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 markedtry.
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:
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:
Fatal error: Error raised at top level: main.ParseError.emptyThe 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:
func price(_ weight: String) -> Int { // ✗
let kg = try parse(weight)
return kg * 3
}error: errors thrown from here are not handledThe 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
throwsto the header and pass the error on; - catch the error inside the function with its own
do/catch, ending it with acatchwithout a pattern.
Key points
- A function with
throwsmay pass an error on instead of catching it: the call inside it is written withtry, and the error goes to the caller. - An error removes every call on its path until the nearest
catchcatches it; the lines belowtrydo not run. catch ParseError.emptycatches one case, and acatchwithout a pattern catches all the others, with the error in the constanterror.- An error nobody caught crashes the program:
Fatal error: Error raised at top level.