Magical Error Handling in Swift

In this tutorial you will learn all about error handling in Swift. You’ll learn about all the new features added in Swift 2.0 and discover how to use them. By Gemma Barlow.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Avoiding Errors with Custom Handling

Having cleaned up the Spell initializer and avoided some errors through the clever use of nil, you’re ready to tackle some more intricate error handling.

For the next section of this tutorial, open up Avoiding Errors with Custom Handling – Starter.playground.

Take note of the following features of the code:

struct Spell {
  var magicWords: MagicWords = .abracadbra

  init?(words: String) {
    guard let incantation = MagicWords(rawValue: words) else {
      return nil
    }
    self.magicWords = incantation
  }

  init?(magicWords: MagicWords) {
    self.magicWords = magicWords
  }
}

This is the Spell initializer, updated to match the work you completed in the first section of this tutorial. Also note the presence of the Avatar protocol, and a second failable initializer, which has been added for convenience.

protocol Familiar: Avatar {
  var noise: String { get }
  var name: String? { get set }
  init(name: String?)
}

The Familiar protocol will be applied to various animals (such as bats and toads) further down in the playground.

Owl

This clearly isn’t Hedwig, but still cute nonetheless, no?

Note: For those unfamiliar with the term familiar, this is a witch’s or wizard’s magical animal sidekick, which usually has human-like qualities. Think Hedwig from Harry Potter, or the flying monkeys in the Wizard of Oz.
struct Witch: Magical {
  var avatar = "*"
  var name: String?
  var familiar: Familiar?
  var spells: [Spell] = []
  var hat: Hat?

  init(name: String?, familiar: Familiar?) {
    self.name = name
    self.familiar = familiar

    if let s = Spell(magicWords: .prestoChango) {
      self.spells = [s]
    }
  }

  init(name: String?, familiar: Familiar?, hat: Hat?) {
    self.init(name: name, familiar: familiar)
    self.hat = hat
  }

  func turnFamiliarIntoToad() -> Toad {
    if let hat = hat {
      if hat.isMagical { // When have you ever seen a Witch perform a spell without her magical hat on ? :]
        if let familiar = familiar {   // Check if witch has a familiar
          if let toad = familiar as? Toad {  // If familiar is already a toad, no magic required
            return toad
          } else {
            if hasSpell(ofType: .prestoChango) {
              if let name = familiar.name {
                return Toad(name: name)
              }
            }
          }
        }
      }
    }
    return Toad(name: "New Toad")  // This is an entirely new Toad.
  }

  func hasSpell(ofType type: MagicWords) -> Bool { // Check if witch currently has an appropriate spell in their spellbook
    let change = spells.flatMap { spell in
      spell.magicWords == type
    }
    return change.count > 0
  }
}

Finally, the witch. Here you see the following:

  • A Witch is initialized with a name and a familiar, or with a name, a familiar and a hat.
  • A Witch knows a finite number of spells, stored in spells, which is an array of Spell objects.
  • A Witch seems to have a penchant for turning her familiar into a toad via the use of the .prestoChango spell, within turnFamiliarIntoToad().

Notice the length and amount of indentation in turnFamiliarIntoToad(). Also, if anything goes wrong in the method, an entirely new toad will be returned. That seems like a confusing (and incorrect!) outcome for this particular spell. You’ll clean up this code significantly with custom error handling in the next section.

Refactoring to Use Swift Errors

The Swift Programming Language (Swift 3)

“Swift provides first-class support for throwing, catching, propagating, and manipulating
recoverable errors at runtime.”

Not to be confused with the Temple of Doom, the Pyramid of Doom is an anti-pattern found in Swift and other languages that can require many levels of nested statements for control flow. It can be seen in turnFamiliarIntoToad() above – note the six closing parentheses required to close out all the statements, trailing down on a diagonal. Reading code nested in this way requires excessive cognitive effort.

Pyramid of Doom

Guard statements, as you’ve seen earlier, and multiple simultaneous optional bindings can assist with the cleanup of pyramid-like code. The use of a do-catch mechanism, however, eliminates the problem altogether by decoupling control flow from error state handling.

do-catch mechanisms are often found near the following, related, keywords:

  • throws
  • do
  • catch
  • try
  • defer
  • Error

To see these mechanisms in action, you are going to throw multiple custom errors. First, you’ll define the states you wish to handle by listing out everything that could possibly go wrong as an enumeration.

Add the following code to your playground above the definition of Witch:

enum ChangoSpellError: Error {
  case hatMissingOrNotMagical
  case noFamiliar
  case familiarAlreadyAToad
  case spellFailed(reason: String)
  case spellNotKnownToWitch
}

Note two things about ChangoSpellError:

  • It conforms to the Error protocol, a requirement for defining errors in Swift.
  • In the spellFailed case, you can handily specify a custom reason for the spell failure with an associated value.
Note: The ChangoSpellError is named after the magical utterance of “Presto Chango!” – frequently used by a Witch when attempting to change a familiar into a Toad).

OK, ready to make some magic, my pretties? Excellent. Add throws to the method signature, to indicate that errors may occur as a result of calling this method:

func turnFamiliarIntoToad() throws -> Toad {

Update it as well on the Magical protocol:

protocol Magical: Avatar {
  var name: String? { get set }
  var spells: [Spell] { get set }
  func turnFamiliarIntoToad() throws -> Toad
}

Now that you have the error states listed, you will rework the turnFamiliarIntoToad() method, one clause at a time.

Handling Hat Errors

First, modify the following statement to ensure the witch is wearing her all-important hat:

if let hat = hat {

…to the following:

guard let hat = hat else {
  throw ChangoSpellError.hatMissingOrNotMagical
}
Note: Don’t forget to remove the associated } at the bottom of the method, or else the playground will compile with errors!

The next line contains a boolean check, also associated with the hat:

if hat.isMagical {

You could choose to add a separate guard statement to perform this check, but it would be clearer to group the checks together on a single line. As such, change the first guard statement to match the following:

guard let hat = hat, hat.isMagical else {
  throw ChangoSpellError.hatMissingOrNotMagical
}

Now remove the if hat.isMagical { check altogether.

In the next section, you’ll continue to unravel the conditional pyramid.

Gemma Barlow

Contributors

Gemma Barlow

Author

Over 300 content creators. Join our team.