Throw-Catch

In the Lesson01-Review-Basics playground, click Next to open the Lesson01-throw-catch page.

Bakery Model

Here’s a model for a bakery that sells pastries. The itemsForSale dictionary contains Pastry objects, each with flavor and numberOnHand properties.

class Pastry {
  let flavor: String
  var numberOnHand: Int
    
  init(flavor: String, numberOnHand: Int) {
    self.flavor = flavor
    self.numberOnHand = numberOnHand
  }
}

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(_ shouldOpen: Bool = Bool.random()) -> Bool {
    return shouldOpen
  }
    
  func orderPastry(item: String, amountRequested: Int, flavor: String) -> Int {
    let pastry = itemsForSale[item]
    pastry.numberOnHand -= amountRequested
    return pastry.numberOnHand
  }
}

To use this model, create a Bakery object, then call open(_:) and orderPastry(item:amountRequested:flavor:)

let bakery = Bakery()
bakery.open()
bakery.orderPastry(item: "Cookie", amountRequested: 1, flavor: "ChocolateChip")

What are the possible error situations that might arise?

  • The bakery can’t open if it has no pastries to sell, or no electricity.
  • item must be one of the keys of itemsForSale.
  • For the specified item, the flavor argument must be a valid flavor.
  • For the specified item, amountRequested must be less than or equal to numberOnHand

Error Protocol

Swift has an Error protocol, which forms the basis of its error-handling architecture. Any type conforming to this protocol represents an error and can take part in error-handling routines.

Any named type can conform to Error but it’s especially well-suited to enumerations. Define an enum for the possible Bakery errors that can happen:

enum BakeryError: Error {
  case noInventory, noPower
  case tooFew(numberOnHand: Int), noSuchItem, wrongFlavor
}

Now, you can check for these error conditions in open(_:) and orderPastry(item:amountRequested:flavor:). If an error condition occurs, you throw the appropriate BakeryError.

Throwing an Error

Edit open(_:) to throw noInventory or noPower:

func open(_ shouldOpen: Bool = Bool.random()) throws -> Bool {  // 1
  guard shouldOpen else {  
    throw Bool.random() ? BakeryError.noInventory : BakeryError.noPower  // 2
  }
  return shouldOpen
}
  1. You mark the function with the keywork throws to tell the compiler that this function might throw an error.
  2. If shouldOpen is false, you throw either BakeryError.noInventory or BakeryError.noPower

orderPastry(item:amountRequested:flavor:) can throw one of the other three errors:

func orderPastry(item: String, amountRequested: Int, flavor: String) throws -> Int {  // 1
  guard let pastry = itemsForSale[item] else {  // 2
    throw BakeryError.noSuchItem
  }
  guard flavor == pastry.flavor else {  // 3
    throw BakeryError.wrongFlavor
  }
  guard amountRequested <= pastry.numberOnHand else {  // 4
    throw BakeryError.tooFew(numberOnHand: pastry.numberOnHand)
  }
  pastry.numberOnHand -= amountRequested
      
  return pastry.numberOnHand
}
  1. You tell the compiler that this function might throw an error.
  2. First, you check that item is a key of itemsForSale. If it isn’t, pastry is nil, and none of the other code makes sense. You throw BakeryError.noSuchItem.
  3. Next, you check that the flavor argument matches the item’s flavor. If it doesn’t, you throw BakeryError.wrongFlavor.
  4. Finally, you check there are enough on hand to fill the order. If not, you throw BakeryError.tooFew(numberOnHand:).

Catching an Error

If your code calls a function that can throw an error, you usually want to catch and handle any errors it throws. To do this, you try to call it, inside a do closure. Enclose the calls to bakery.open and bakery.orderPastry in a do closure and insert the keyword try:

do {
  try bakery.open()
  try bakery.orderPastry(item: "Cookie", amountRequested: 1, flavor: "Butter")
}

You follow a do closure with one or more catch closures, to handle any thrown errors. Enter the following code to handle BakeryError cases in a switch statement:

catch let error as BakeryError {
  switch error {
  case .noInventory, .noPower:
    print("Sorry, the bakery is now closed.")
  case .noSuchItem:
    print("Sorry, but we don't sell this item.")
  case .wrongFlavor:
    print("Sorry, but we don't carry this flavor.")
  case .tooFew(numberOnHand: let items):
    print("We only have \(items) of that item.")
  }
}

You catch any BakeryError thrown by open(_:) or orderPastry(item:amountRequested:flavor:), then print a message about what went wrong.

Note: You must cast error as BakeryError so you can switch over its cases.

If the code in the do closure can throw any other specific type of Error, you can handle those in other catch closures. You also need to catch any other error — add this catch closure to do that:

catch {
  print("Something went wrong: \(error)")
}

Run this code. If the bakery is closed, pass true to bakery.open(_:) and run the code again.

Wrong flavor
Wrong flavor

The bakery doesn’t sell butter cookies, so orderPastry(item:amountRequested:flavor:) throws .wrongFlavor.

Alternatively, instead of the switch statement, you could handle each BakeryError in a separate catch closure. Uncomment and run this code:

do {
  try bakery.open()
  try bakery.orderPastry(item: "Albatross", amountRequested: 1, flavor: "AlbatrossFlavor")
}
// Another way to handle every BakeryError
catch BakeryError.noInventory, BakeryError.noPower {
  print("Sorry, the bakery is now closed.")
} catch BakeryError.noSuchItem {
  print("Sorry, but we don't sell this item.")
} catch BakeryError.wrongFlavor {
  print("Sorry, but we don't carry this flavor.")
} catch BakeryError.tooFew(numberOnHand: let items) {
  print("Sorry, we only have \(items) of that item.")
} catch {
  print("Some other error.")
}

No such item
No such item

This time, orderPastry(item:amountRequested:flavor:) throws .noSuchItem , because it checks that condition first.

Note: You must fully specify each error — BakeryError.noPoweretc. — and still catch any other errors.

Not Caring About an Error

If you don’t care about the error details, you don’t need do and catch closures. Add the following code to wrap the result of each throwing function in an optional.

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

Run the code:

try? returns nil.
try? returns nil.

Both throwing functions return nil.

Knowing Errors Won’t Happen

If you know your code is never going to fail, or you want your program to terminate if the function throws an error, use try! — add the following code and run it:

try! bakery.open(true)
try! bakery.orderPastry(item: "Cookie", amountRequested: 1, flavor: "Butter")

try! throws error and crashes.
try! throws error and crashes.

As you know, the bakery doesn’t sell butter cookies, so orderPastry(item:amountRequested:flavor:) throws .wrongFlavor, then stops execution.

Note: Using try! has the same effect as:

do {
  try bakery.open(true)
  try bakery.orderPastry(item: "Cookie", amountRequested: 1, flavor: "Butter")
}
catch {
  fatalError()
}

The Problem With throws

You want the compiler to help you write the best possible code. Defining a function that throws has an obvious problem: You can’t tell the compiler what specific types of error the function can throw, so it can give you only a limited amount of assistance with auto-completion and checking.

  • To switch over the cases of a specific Error type, you must cast error as that type.
  • Or, you must specify the type of Error for each catch closure, even if the throwing function throws only one type of Error.
  • And you must always catch any other errors, even when you know the throwing function never throws any other type of Error.

In the next lesson, you’ll see how typed throws overcome these issues.

See forum comments
Download course materials from Github
Previous: Optionals Next: Conclusion