Home iOS & Swift Books Swift Apprentice

21
Error Handling Written by Cosmin Pupăză

All programmers, especially skilled ones, need to worry about error handling. There is no shame in errors. They don’t mean you’re a terrible 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. Although you have complete control of your code, you don’t control outside events and resources. These include user input, network connections and 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 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 to the fact that you have no internet access?

When you’re designing the user experience for your apps, you must think about the error states. 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 most straightforward error-handling mechanism possible. When programming, it’s essential 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 tells you that you’re dealing with an optional value and sometimes suggests 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 represents an error.

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
  case inventory
  case noPower
}

Throwing errors

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 open(_ now: Bool = Bool.random()) throws -> Bool {
    guard now else {
      throw Bool.random() ? BakeryError.inventory 
                          : BakeryError.noPower
    }
    return now
  }

  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.open()
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 immediately handle your errors or bubble them up to another level.

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

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 instead of throwing an error. No need to set up a do {} catch {} block.

let open = try? bakery.open(false)
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 you certainly know the bakery is now open and you just restocked the cookie jar, you’ll be able to order a cookie. Add:

do {
  try bakery.open(true)
  try bakery.orderPastry(item: "Cookie",
                         amountRequested: 1,
                         flavor: "ChocolateChip")
}
catch {
  fatalError()
}
try! bakery.open(true)
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 a more extensive, more 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, it just passes the error up to the caller.

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 choose: 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 to 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 is a closure parameter that can be stored and called after the 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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.