Chapters

Hide chapters

Swift Apprentice

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Advanced Topics

Section 4: 10 chapters
Show chapters Hide chapters

21. Error Handling
Written by Cosmin Pupăză

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

All programmers, especially skilled ones, need to worry about error handling. There is no shame in errors. They don’t mean you’re a bad programmer. Concerning yourself with error handling simply means you acknowledge that you don’t control everything.

In this chapter, you’ll learn the fundamentals of error handling: what it is, how to implement and when to worry about it.

What is error handling?

Error handling is the art of failing gracefully. You have complete control of your code, but you don’t have complete control of anything outside of your code. This includes user input, network connections and any external files your app needs to access.

Imagine you’re in the desert and you decide to surf the internet. You’re miles away from the nearest hotspot. You have 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 to the fact that you have no internet access?

These are things you need to consider when you’re designing the user experience for your apps, as well as the interfaces of your classes and structs. Think about what can go wrong, and how you want your app to respond to it.

First level error handling with optionals

Before you deep-dive into error handling protocols and blocks, you’ll start with the simplest error-handling mechanism possible. When programming, it’s important to use the simplest solution at your disposal. There is no point in building a complicated solution when changing one line of code would work.

Failable initializers

When you attempt to initialize an object, 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
enum PetFood: String {
  case kibble, canned
}

let morning = PetFood(rawValue: "kibble")  // Optional(.kibble)
let snack = PetFood(rawValue: "fuuud!")    // nil
struct PetHouse {
  let squareFeet: Int
  
  init?(squareFeet: Int) {
    if squareFeet < 1 {
      return nil
    }
    self.squareFeet = squareFeet
  }
}

let tooSmall = PetHouse(squareFeet: 0) // nil
let house = PetHouse(squareFeet: 1)    // Optional(Pethouse)

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 is telling you that you’re dealing with an optional value and suggesting that you deal with it by force unwrapping.

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!
if let dogBreed = janie.pet.breed {
  print("Olive is a \(dogBreed).")
} else {
  print("Olive’s breed is unknown.")
}
class Toy {

  enum Kind {
    case ball
    case zombie
    case bone
    case mouse
  }

  enum Sound {
    case squeak
    case 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
    case cat
    case 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
  }
}

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()
if let sound = janie.pet?.favoriteToy?.sound {
  print("Sound \(sound).")
} else {
  print("No sound.")
}
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.")
}

Map and compactMap

Let’s say you want to create an array of pets that are owned by the team. First off, you need to create an array of team members:

let team = [janie, tammy, felipe]
let petNames = team.map { $0.pet?.name }
for pet in petNames {
  print(pet)
}
Optional("Delia")
Optional("Evil Cat Overlord")
nil

let betterPetNames = team.compactMap { $0.pet?.name }

for pet in betterPetNames {
  print(pet)
}
Delia
Evil Cat Overlord

Error protocol

Swift includes the Error protocol, which forms the basis of the error-handling architecture. Any type that conforms to this protocol can be used to represent errors.

class Pastry {
  let flavor: String
  var numberOnHand: Int

  init(flavor: String, numberOnHand: Int) {
    self.flavor = flavor
    self.numberOnHand = numberOnHand
  }
}

enum BakeryError: Error {
  case tooFew(numberOnHand: Int)
  case doNotSell
  case wrongFlavor
}

Throwing errors

This is kind of cool, but what does your program do with these errors? It throws them, of course! That’s the actual terminology you’ll see: throwing errors then catching them.

class Bakery {
  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)
  ]

  func orderPastry(item: String,
                   amountRequested: Int,
                   flavor: String)  throws  -> Int {
    guard let pastry = itemsForSale[item] else {
      throw BakeryError.doNotSell
    }
    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
  }
}
let bakery = Bakery()
bakery.orderPastry(item: "Albatross",
                   amountRequested: 1,
                   flavor: "AlbatrossFlavor")

Handling errors

After your program throws an error, you need to handle that error. There are two ways to approach this problem: You can handle your errors immediately, or you can bubble them up to another level.

do {
  try bakery.orderPastry(item: "Albatross",
                          amountRequested: 1,
                          flavor: "AlbatrossFlavor")
} 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.")
}

Not looking at the detailed error

If you don’t really care about the details of the error you can use try? to wrap the result of a function (or method) in an optional. The function will then return nil instead of throwing an error. No need to setup a do {} catch {} block.

let remaining = try? bakery.orderPastry(item: "Albatross",
                                        amountRequested: 1,
                                        flavor: "AlbatrossFlavor")

Stoping your program on an error

Sometimes you know for sure that your code is not going to fail. For example, if know you just restocked the cookie jar, you know you’ll be able to order a cookie. Add:

do {
  try bakery.orderPastry(item: "Cookie",
                         amountRequested: 1,
                         flavor: "ChocolateChip")
}
catch {
  fatalError()
}
try! bakery.orderPastry(item: "Cookie", amountRequested: 1, 
                        flavor: "ChocolateChip")

Advanced error handling

Cool, you know how to handle errors! That’s neat, but how do you scale your error handling to the larger context of a 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 it gets lost and confused.

enum Direction {
  case left
  case right
  case forward
}
enum PugBotError: Error {
  case invalidMove(found: Direction, expected: Direction)
  case endOfPath
}

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

Handling multiple errors

Since you’re a smart developer, you’ve noticed that you’re not handling errors in goHome(). Instead, you’ve marked that function with throws as well, leaving the error handling up to the caller of the function.

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."
  }
}
pug.reset()
moveSafely(goHome)

pug.reset()
moveSafely {
  try pug.move(.forward)
  try pug.move(.left)
  try pug.move(.forward)
  try pug.move(.right)
}

Rethrows

A function that takes a throwing closure as a parameter has to make a choice: either catch every error or be a throwing function. Let’s say you want a utility function to perform a certain 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()
  }
}

Error handling for asynchronous code

The do-try-catch mechanism works only for synchronous code. You can’t use throws to throw errors if you execute your code asynchronously. Swift has you covered, but you first need to understand how to work with asynchronous closures and Grand Central Dispatch (GCD).

GCD

Modern operating environments are multi-threaded, meaning work can happen simultaneously on multiple threads of execution. For example, all networking operations execute in a background thread so they don’t block the user interface that happens on the main thread.

//1
func log(message: String) {
  let thread = Thread.current.isMainThread ? "Main" 
               : "Background"
  print("\(thread) thread: \(message).")
}

//2
func addNumbers(upTo range: Int) -> Int {
  log(message: "Adding numbers...")
  return (1...range).reduce(0, +)
}
let queue = DispatchQueue(label: "queue")
// 1
func execute<Result>(backgroundWork: @escaping () -> Result,
                     mainWork: @escaping (Result) -> ()) {
  // 2
  queue.async {
    let result = backgroundWork()
    // 3
	DispatchQueue.main.async {
      mainWork(result)
    }
  }
}
execute(backgroundWork: { addNumbers(upTo: 100) },
        mainWork:       { log(message: "The sum is \($0)") })
Background thread: Adding numbers...
Main thread: The sum is 5050.

Result

You use the Result type defined in the Swift standard library to capture errors thrown by asynchronous functions. Here’s how it is defined:

enum Result<Success, Failure> where Failure: Error {
  case success(Success)
  case failure(Failure)
}
// 1
struct Tutorial {
  let title: String
  let author: String
}

// 2
enum TutorialError: Error {
  case rejected
}

// 3
func feedback(for tutorial: Tutorial) -> Result<String, 
                                                TutorialError> {
  Bool.random() ? .success("published") : .failure(.rejected)
}
func edit(_ tutorial: Tutorial) {
  queue.async {
    // 1
    let result = feedback(for: tutorial)
    DispatchQueue.main.async {
      switch result {
        // 2
        case let .success(data):
          print("\(tutorial.title) by \(tutorial.author) was 
                 \(data) on the website.")
        // 3
        case let .failure(error):
          print("\(tutorial.title) by \(tutorial.author) was 
                 \(error).")
      }
    }
  }
}

let tutorial = Tutorial(title: "What’s new in Swift 5.1", 
                        author: "Cosmin Pupăză")
edit(tutorial)
let result = feedback(for: tutorial)
do {
  let data = try result.get()
  print("\(tutorial.title) by \(tutorial.author) was 
         \(data) on the website.")
} catch {
  print("\(tutorial.title) by \(tutorial.author) was \(error).")
}

Challenges

Before moving on, here are some challenges to test your knowledge of error handling. It is best if you try to 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 throwing function that converts a String to an even number, rounding down if necessary.

Challenge 2: Safe division

Write a throwing function that divides type Int types.

Key points

  • 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, you must embed the function call in a do block. Within that block, you try the function, and if it fails, you catch the error.
  • You use GCD and Result to handle errors asynchronously.
  • An escaping closure can be used after the corresponding function returns.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2023 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now