Home iOS & Swift Books Swift Apprentice

22
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.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Skilled developers design their software for errors. 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, 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 it.

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 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, how you want your app to respond, and how you want 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
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, 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
  }
}

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 the team owns. 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), doNotSell, wrongFlavor
  case inventory, 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: Immediately handling 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, right, 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 savvy 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.

func perform(times: Int, movement: () throws -> ()) rethrows {
  for _ in 1...times {
    try movement()
  }
}

Throwable properties

You can throw errors from read-only computed properties:

// 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
}

// 3
extension Person {
  var data: String {
    get throws {
      guard !name.isEmpty else {throw PersonError.noName}
      guard age > 0 else {throw PersonError.noAge}
      return "\(name) is \(age) years old."
    }
  }
}
let me = Person(name: "Cosmin", age: 36)

me.name = ""
do {
  try me.data
} catch {
  print(error) // "noName"
}

me.age = -36
do {
  try me.data
} catch {
  print(error) // "noName"
}

me.name = "Cosmin"
do {
  try me.data
} catch {
  print(error) // "noAge"
}

me.age = 36
do {
  try me.data // "Cosmin is 36 years old."
} catch {
  print(error)
}

Throwable subscripts

You can also throw errors from read-only subscripts:

extension Person {
  subscript(key: String) -> String {
    get throws {
      switch key {
        case "name": return name
        case "age": return "\(age)"
        default: throw PersonError.noData
      }
    }
  }
}
do {
  try me["name"] // "Cosmin"
} catch {
  print(error)
}

do {
  try me["age"] // "36"
} catch {
  print(error)
}

do {
  try me["gender"] 
} catch {
  print(error) // "noData"
}

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 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.
  • Read-only computed properties and subscripts can throw errors.

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:

© 2021 Razeware LLC

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 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.