5.
Error Handling
Written by Matt Galloway
Skilled developers design their software for errors. Error handling is the art of failing gracefully. Although you completely control your code, you don’t control outside events and resources. These include user input, network connections, available system memory and files your app needs to access.
In this chapter, you’ll learn the fundamentals of error handling: what it is and different strategies for implementing good error handling.
What is Error Handling?
Imagine you’re in the desert, and you decide to surf the internet. You’re miles away from the nearest hotspot with no cellular signal. You open your internet browser. What happens? Does your browser hang there forever with a spinning wheel of death, or does it immediately alert you that you have no internet access?
When designing the user experience for your apps, you must think about the error states. Think about what can go wrong, how you want your app to respond, and how to surface that information to users to allow them to act on it appropriately.
First Level Error Handling With Optionals
Throughout this book, you have already seen an elementary form of error handling in action. Optionals model missing information and provide compiler and runtime guarantees that you won’t accidentally act on values that are not available. This predictability is the foundation of Swift’s safety.
Failable Initializers
When you try to initialize an object from external input, it may fail. For example, if you’re converting a String into an Int, there is no guarantee it’ll work.
let value = Int("3") // Optional(3)
let failedValue = Int("nope") // nil
In “Swift Apprentice: Fundamentals - Chapter 16: Enumerations,” you saw that if you make your own raw representable enumeration type, the compiler creates a failable initializer for you. For example, suppose you have some pet foods backed by a string, like so:
enum PetFood: String {
case kibble, canned
}
let morning = PetFood(rawValue: "kibble") // Optional(.kibble)
let snack = PetFood(rawValue: "fuuud!") // nil
The return type is optional to recognize the risk of failure, and the return value will be nil if initialization fails.
You can create failable initializers yourself. Try it out:
struct PetHouse {
let squareFeet: Int
init?(squareFeetAsString: String) {
guard let squareFeet = Int(squareFeetAsString) else {
return nil
}
self.squareFeet = squareFeet
}
}
let nopeHouse = PetHouse(squareFeetAsString: "nope") // nil
let house = PetHouse(squareFeetAsString: "100") // Optional(Pethouse)
To make a failable initializer, name it init?(...) and return nil if it fails. Using a failable initializer, you can guarantee that your instance has the correct attributes, or it will never exist.
Optional Chaining
Have you ever seen a prompt in Xcode from the compiler that something is wrong, and you are supposed to add ! to a property? The compiler tells you you’re dealing with an optional value and sometimes suggests you deal with it by force unwrapping.
Sometimes force unwrapping or using an implicitly unwrapped optional is just fine. If you have @IBOutlets in your UIKit app, you know those elements must exist after the view loads, and if they don’t, there is something wrong with your app. In general, force unwrap or using implicitly unwrapped optionals is appropriate only when an optional must contain a value. In all other cases, you’re asking for trouble!
Consider this code:
class Pet {
var breed: String?
init(breed: String? = nil) {
self.breed = breed
}
}
class Person {
let pet: Pet
init(pet: Pet) {
self.pet = pet
}
}
let delia = Pet(breed: "pug")
let olive = Pet()
let janie = Person(pet: olive)
let dogBreed = janie.pet.breed! // This is bad! Will cause a crash!
In this simple example, olive has no breed. She was a rescue from the pound, so her breed is unknown. But she’s still a sweetheart.
If you assume she has a breed and force unwraps this property, it will cause the program to crash. There’s a better way of handling this situation:
if let dogBreed = janie.pet.breed {
print("Olive is a \(dogBreed).")
} else {
print("Olive’s breed is unknown.")
}
This code is standard optional handling, but you can take you far even with more complicated types with nested optionals.
Comment out what you have so far and start over with the following types:
class Toy {
enum Kind {
case ball, zombie, bone, mouse
}
enum Sound {
case squeak, bell
}
let kind: Kind
let color: String
var sound: Sound?
init(kind: Kind, color: String, sound: Sound? = nil) {
self.kind = kind
self.color = color
self.sound = sound
}
}
class Pet {
enum Kind {
case dog, cat, guineaPig
}
let name: String
let kind: Kind
let favoriteToy: Toy?
init(name: String, kind: Kind, favoriteToy: Toy? = nil) {
self.name = name
self.kind = kind
self.favoriteToy = favoriteToy
}
}
class Person {
let pet: Pet?
init(pet: Pet? = nil) {
self.pet = pet
}
}
A lot of Kodeco team members own pets — but not all. Some pets have a favorite toy, and others don’t. Some of these toys make noise, and others don’t.
For example, Tammy Coron’s evil cat is methodically plotting her death.
This cat’s favorite toy to chew on (besides Tammy) is a catnip mouse. This toy doesn’t make any noise.
Felipe Marsetti is a Kodeco team member who lives in a condo and isn’t allowed to have pets.
let janie = Person(pet: Pet(name: "Delia", kind: .dog,
favoriteToy: Toy(kind: .ball,
color: "Purple", sound: .bell)))
let tammy = Person(pet: Pet(name: "Evil Cat Overlord",
kind: .cat, favoriteToy: Toy(kind: .mouse,
color: "Orange")))
let felipe = Person()
You want to check if any team members have a pet with a favorite toy that makes a sound. You can use optional chaining for this; it’s a quick way to walk through a chain of optionals by adding a ? after every property or method that can return nil. If any of the chain’s values are nil, the result will be nil. So instead of having to test every optional along the chain, you simply test the result!
For example:
if let sound = janie.pet?.favoriteToy?.sound {
print("Sound \(sound).")
} else {
print("No sound.")
}
Janie’s pet — one of her pugs, not just any old pet — fulfills all of the conditions, and therefore the sound is accessible.
Try accessing the sound with Tammy and Felipe:
if let sound = tammy.pet?.favoriteToy?.sound {
print("Sound \(sound).")
} else {
print("No sound.")
}
if let sound = felipe.pet?.favoriteToy?.sound {
print("Sound \(sound).")
} else {
print("No sound.")
}
During each stage of this chain, you check whether each optional property is present. If any of the values are nil along the way, the result is also nil.
Since Tammy’s cat’s toy does not have a sound, the optional chain bails out after favoriteToy? and returns nil. Since Felipe doesn’t have a pet, the process bails out after pet?.
All this checking is repetitive. What if you wanted to iterate through the entire array of team members to find this information?
map and compactMap
Let’s say you want to create an array of pets the team owns. First off, you need to create an array of team members:
let team = [janie, tammy, felipe]
You want to iterate through this array and extract all pet names. You could use a for loop, but you’ve already learned a better way to do this: map.
let petNames = team.map { $0.pet?.name }
This code creates a new array of pet names by pulling out the pet name from each team member in the array. You want to see what these values are, so why not print them out?
for pet in petNames {
print(pet)
}
The compiler generates a warning:
Expression implicitly coerced from 'String?' to 'Any'
Now look at the output in the console for this print statement:
Optional("Delia")
Optional("Evil Cat Overlord")
nil
Ew! That doesn’t look right.
Instead of having a nice list of names, you have many optional values and even a nil. This won’t do at all.
You could take this array, filter it and then call map again to unwrap all the values that are not nil, but that seems somewhat convoluted. Iterating through an array of optional values you need to unwrap and ensure they are not nil is a common operation.
There is a better way to accomplish this task: compactMap. Try out the following:
let betterPetNames = team.compactMap { $0.pet?.name }
for pet in betterPetNames {
print(pet)
}
You should see a far more helpful and user-friendly output:
Delia
Evil Cat Overlord
compactMap does a regular map operation and potentially “compacts” or shrinks the result array’s size. In this case, you’re using compactMap to compact the return type [Optional<String>] into the type [String].
So far, you’ve learned how to do some informal error handling. Up next, you’ll learn about the Error protocol to do some proper error handling.
Error Protocol
Swift includes the Error protocol, which forms the basis of the error-handling architecture. Any type conforming to this protocol represents an error and can take part in error-handling routines you will learn about shortly.
Any named type can conform to Error but is especially well-suited to enumerations. Let’s try it out now.
Create a new playground where you will create an abstraction for a bakery and use it to learn how to throw and handle errors.
Add this code to your playground:
class Pastry {
let flavor: String
var numberOnHand: Int
init(flavor: String, numberOnHand: Int) {
self.flavor = flavor
self.numberOnHand = numberOnHand
}
}
This class will hold different items that you’ll sell at your bakery.
Then add the following code to your playground:
enum BakeryError: Error {
case tooFew(numberOnHand: Int), doNotSell, wrongFlavor
case inventory, noPower
}
Here you’re defining an enum that conforms to Error. The Error protocol tells the compiler that this enumeration represents errors you can throw.
There are many types of errors at a bakery. You may be out of stock, have the wrong flavor, or not sell an item altogether. The bakery may also be closed because it ran out of inventory or because of a power outage.
Throwing Errors
What does your program do with these errors? It throws them, of course! That’s the terminology you’ll see: throwing errors and then catching them.
Add this class to your playground:
class Bakery {
// 1
var itemsForSale = [
"Cookie": Pastry(flavor: "ChocolateChip", numberOnHand: 20),
"PopTart": Pastry(flavor: "WildBerry", numberOnHand: 13),
"Donut" : Pastry(flavor: "Sprinkles", numberOnHand: 24),
"HandPie": Pastry(flavor: "Cherry", numberOnHand: 6)
]
// 2
func open(_ shouldOpen: Bool = Bool.random()) throws -> Bool {
guard shouldOpen else {
// 3
throw Bool.random() ? BakeryError.inventory
: BakeryError.noPower
}
return shouldOpen
}
// 4
func orderPastry(item: String,
amountRequested: Int,
flavor: String) throws -> Int {
// 5
guard let pastry = itemsForSale[item] else {
throw BakeryError.doNotSell
}
// 6
guard flavor == pastry.flavor else {
throw BakeryError.wrongFlavor
}
guard amountRequested <= pastry.numberOnHand else {
throw BakeryError.tooFew(numberOnHand:
pastry.numberOnHand)
}
pastry.numberOnHand -= amountRequested
return pastry.numberOnHand
}
}
Here’s what that code does:
-
First, you need to have some items to sell. Each item needs to have a flavor and an amount on hand. When customers order a pastry from you, they need to tell you what pastry they want, what flavor, and how many they want. Customers can be incredibly demanding. :]
-
The bakery sometimes closes because of unexpected inventory shortages or a random power outage. When you open the bakery, check for these.
-
Here you want to throw your first error. When you shouldn’t open, you
throw, giving a random error betweenBakeryError.inventoryandBakeryError.noPower. -
You need a method to allow someone to place an order.
-
When placing an order, first, you need to check if you even carry what the customer wants. You don’t want the bakery to crash if the customer tries to order albatross with wafers. If you don’t carry that item, you
throwtheBakeryError.doNotSellerror. -
After verifying that the bakery carries the item the customer wants, you need to check if you have enough of the requested flavor to fulfill the customer’s order. In this case, you
throwtheBakeryError.wrongFlavorerror.
As this example shows, you throw errors using throw. The errors you throw must be instances of a type that conforms to Error. A function (or method) that throws errors and does not immediately handle them must clarify this by adding throws to its declaration.
Next, try out your bakery:
let bakery = Bakery()
bakery.open()
bakery.orderPastry(item: "Albatross",
amountRequested: 1,
flavor: "AlbatrossFlavor")
The code above does not compile. You’ll get the following error:
Call can throw but is not marked with 'try'
What’s wrong? Oh, right — you need to catch the error and do something with it!
Handling Errors
After your program throws an error, you need to handle that error. There are two ways to approach this problem: Immediately handling your errors or bubble them up to another level.
To choose your approach, you need to consider where it makes the most sense to handle the error. If it makes sense to handle the error immediately, then do so. Suppose you’re in a situation where you have to alert the user and have her take action, but you’re several function calls away from a user interface element. In that case, it makes sense to bubble up the error until you reach the point where you can alert the user.
It’s up to you at what level in your call stack to handle the error, but not handling it isn’t an option. Swift requires you to deal with the error at some point in the chain, or your program won’t compile.
Replace the previous line of code with this:
do {
try bakery.open()
try bakery.orderPastry(item: "Albatross",
amountRequested: 1,
flavor: "AlbatrossFlavor")
} catch BakeryError.inventory, BakeryError.noPower {
print("Sorry, the bakery is now closed.")
} catch BakeryError.doNotSell {
print("Sorry, but we don’t sell this item.")
} catch BakeryError.wrongFlavor {
print("Sorry, but we don’t carry this flavor.")
} catch BakeryError.tooFew {
print("Sorry, we don’t have enough items to fulfill your
order.")
}
Code that can throw errors must always be inside a do block, which creates a new scope. Even more, the possible points where errors can occur have a try in front of them. The try serves as a reminder to anyone reading your code that something could go wrong.
You’re now catching each error condition and providing helpful feedback to the user about why the bakery is closed for now and why you can’t fulfill their order. You can catch multiple errors in the same catch block - really cool! :]
Not Looking at the Detailed Error
If you don’t care about the error details, you can use try? to wrap the result of a function (or method) in an optional. The function will then return nil if an error is thrown within it. In this case, there is no need to set up a do {} catch {} block.
For example:
let open = try? bakery.open(false)
let remaining = try? bakery.orderPastry(item: "Albatross",
amountRequested: 1,
flavor: "AlbatrossFlavor")
Here you’re opening a bakery and forcing it to throw when calling open (because the parameter is false). And the call to orderPastry will also throw an error because Albatross is not a valid item!
This code is nice and short to write, but the downside is that you don’t get any details if the request fails. That may be fine for your use case, or it may not. Using try? is useful, but be sure to use try and catch if you want to know specifically why something failed.
Stopping Your Program on an Error
Sometimes you know for sure that your code is not going to fail. For example, if you know the bakery is now open and just restocked the cookie jar, you can order a cookie. Add:
do {
try bakery.open(true)
try bakery.orderPastry(item: "Cookie",
amountRequested: 1,
flavor: "ChocolateChip")
}
catch {
fatalError()
}
Swift gives you a quick way to write the same thing:
try! bakery.open(true)
try! bakery.orderPastry(item: "Cookie", amountRequested: 1,
flavor: "ChocolateChip")
The try! is much like force unwrapping an optional. And just like force unwrapping an optional, you should use try! carefully. Only use it when you want to program to terminate if the call throws. Avoid using this in production code.
Advanced Error Handling
Cool, you know how to handle errors! That’s neat, but how do you scale your error handling to a more extensive, complex app?
PugBot
The sample project you’ll work with in this second half of the chapter is PugBot. The PugBot is cute and friendly but sometimes gets lost and confused.
As the programmer of the PugBot, it’s your responsibility to ensure it doesn’t get lost on the way home from your PugBot lab.
You’ll learn how to make sure your PugBot finds its way home by throwing an error if it steers off course.
Create a new playground.
First, you need to set up an enum containing all of the directions your PugBot can move:
enum Direction {
case left, right, forward
}
You’ll also need an error type to indicate what can go wrong:
enum PugBotError: Error {
case invalidMove(found: Direction, expected: Direction)
case endOfPath
}
Here, associated values store additional details about what went wrong. With any luck, you can use these to rescue a lost PugBot!
Last but not least, create your PugBot class:
class PugBot {
let name: String
let correctPath: [Direction]
private var currentStepInPath = 0
init(name: String, correctPath: [Direction]) {
self.correctPath = correctPath
self.name = name
}
func move(_ direction: Direction) throws {
guard currentStepInPath < correctPath.count else {
throw PugBotError.endOfPath
}
let nextDirection = correctPath[currentStepInPath]
guard nextDirection == direction else {
throw PugBotError.invalidMove(found: direction,
expected: nextDirection)
}
currentStepInPath += 1
}
func reset() {
currentStepInPath = 0
}
}
When creating a PugBot, you tell it how to get home by passing it the correct directions. move(_:) causes the PugBot to move in the corresponding direction. If at any point the program notices the PugBot isn’t doing what it’s supposed to do, it throws an error.
Give your PugBot a test:
let pug = PugBot(name: "Pug",
correctPath: [.forward, .left, .forward, .right])
func goHome() throws {
try pug.move(.forward)
try pug.move(.left)
try pug.move(.forward)
try pug.move(.right)
}
do {
try goHome()
} catch {
print("PugBot failed to get home.")
}
Every single command in goHome() must pass for the method to complete successfully. The moment an error is thrown, your PugBot will stop trying to get home and stay put until you come and rescue it.
If one of the calls to pug.move(:) in goHome() throws, then execution of goHome() will immediately throw that error to whoever called goHome(). No more of goHome() will execute.
For example, if the call to pug.move(.left) throws, then the pug will not try to move forward and right as those calls are after the call to pug.move(.left).
Handling Multiple Errors
You might benefit from a function that can move the PugBot and handle errors by reporting what went wrong. Add the following code to your playground:
func moveSafely(_ movement: () throws -> ()) -> String {
do {
try movement()
return "Completed operation successfully."
} catch PugBotError.invalidMove(let found, let expected) {
return "The PugBot was supposed to move \(expected),
but moved \(found) instead."
} catch PugBotError.endOfPath {
return "The PugBot tried to move past the end of the path."
} catch {
return "An unknown error occurred."
}
}
This function takes a movement function, like goHome(), or a closure containing movement function calls and handles any errors thrown.
You might notice that you have to add a default catch case to the end. What gives? You’ve exhausted the cases in your PugBotError enum, so why is the compiler hassling you?
Unfortunately, at this point, Swift’s do-try-catch system isn’t type-specific. There’s no way to tell the compiler that it should only expect errors that are a PugBotError. To the compiler, that isn’t exhaustive because it doesn’t handle every possible error that it knows about, so you still need a default case. Now you can use your function to handle movement safely:
pug.reset()
moveSafely(goHome)
pug.reset()
moveSafely {
try pug.move(.forward)
try pug.move(.left)
try pug.move(.forward)
try pug.move(.right)
}
Thanks to trailing closure syntax, your movement calls are cleanly wrapped in the call to moveSafely(_:). Here, your PugBot will find her way home safely.
rethrows
A function that takes a throwing closure as a parameter has to choose: either catch every error or be a throwing function. Let’s say you want a utility function to perform a specific movement or set of movements several times in a row.
You could define this function as follows:
func perform(times: Int, movement: () throws -> ()) rethrows {
for _ in 1...times {
try movement()
}
}
Notice the rethrows here. This function does not handle errors like moveSafely(_:). Instead, it leaves error handling to the function’s caller, such as goHome(). The above function uses rethrows to indicate that it will only rethrow errors thrown by the closure passed into it and never throw errors of its own.
Swift can be clever, and if you pass it a closure that doesn’t throw, then the call to perform in that instance is not deemed to be throwable. Therefore if you were to call perform by passing a closure that doesn’t throw, you don’t need to catch anything.
Try it out like so:
try? perform(times: 5) {
try pug.move(.forward)
}
perform(times: 5) {
pug.reset()
}
In the first case, you need the try? (or you could try or try!). In the second, you don’t because Swift knows that closure cannot throw.
Throwable Properties
Types can have computed properties. Sometimes a computed property could fail to compute. In those cases, you want to be able to throw an error from the getter. This is possible only from read-only computed properties.
Make a new playground and add the following code:
// 1
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
// 2
enum PersonError: Error {
case noName, noAge, noData
}
Here is what happens in this code:
- Define a
Personclass withnameandageproperties. - Declare a
PersonErrorenumeration with specificPersonerrors.
Then add the following extension on Person:
extension Person {
var description: String {
get throws {
guard !name.isEmpty else {throw PersonError.noName}
guard age > 0 else {throw PersonError.noAge}
return "\(name) is \(age) years old."
}
}
}
Here you define a read-only computed property called description that returns the name and age of the person as a descriptive string. This property will throw errors if either name or age has an invalid value.
Time to see your throwable property in action:
let me = Person(name: "Alice", age: 32)
me.name = ""
do {
try me.description
} catch {
print(error) // "noName"
}
me.age = -36
do {
try me.description
} catch {
print(error) // "noName"
}
me.name = "Alice"
do {
try me.description
} catch {
print(error) // "noAge"
}
me.age = 36
do {
try me.description // "Alice is 32 years old."
} catch {
print(error)
}
It works for all possible cases - way to go!
Throwable Subscripts
You can also throw errors from read-only subscripts. Add the following code to your playground:
extension Person {
subscript(key: String) -> String {
get throws {
switch key {
case "name": return name
case "age": return "\(age)"
default: throw PersonError.noData
}
}
}
}
The above read-only subscript returns either the person’s name or age and throws errors for invalid keys. Go ahead and try it out:
do {
try me["name"] // "Alice"
} catch {
print(error)
}
do {
try me["age"] // "32"
} catch {
print(error)
}
do {
try me["gender"]
} catch {
print(error) // "noData"
}
It works for all possible scenarios - cool!
Challenges
Before moving on, here are some challenges to test your error-handling knowledge. It’s best to try and solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.
Challenge 1: Even Strings
Write a function that converts a String to an even number, rounding down if necessary. It should throw if the String is not a valid number.
Challenge 2: Safe Division
Write a function that divides two Ints. It should throw if the divisor is zero.
Challenge 3: Account Login
Given the following code:
class Account {
let token: String
}
enum LoginError: Error {
case invalidUser
case invalidPassword
}
func onlyAliceLogin(username: String, password: String) throws -> String {
guard username == "alice" else {
throw LoginError.invalidUser
}
guard password == "hunter2" else {
throw LoginError.invalidPassword
}
return "AUTH_TOKEN"
}
Write an initializer for Account that takes a username, password, and a loginMethod closure. The loginMethod closure should take two String parameters and return a String. It should be able to throw. The initializer should call the loginMethod and store the result in the Account’s token property.
The initializer should work when used with the following examples:
let account1 = try? Account(username: "alice", password: "hunter2", loginMethod: onlyAliceLogin)
let account2 = Account(username: "alice", password: "hunter2") { _, _ in
return "AUTH_TOKEN"
}
Key Points
- You can make an initializer failable by naming them
init?and returningnilif they fail. - A type can conform to the Error protocol to work with Swift’s error-handling system.
- Any function that can throw an error, or call a function that can throw an error, has to be marked with throws or rethrows.
- When calling an error-throwing function from a function that doesn’t throw, you must embed the function call in a
doblock. Within that block, youtrythe function, and if it fails, youcatchthe error. -
try?lets you convert a thrown error into anilreturn value. -
try!lets you convert a thrown error to a fatal error that terminates your app. -
Read-only computed properties and subscripts can be annotated to throw. To access these properties, the standard
try-catchrules apply.