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 3 of 4 of this article. Click here to view the first page.

Handling Familiar Errors

Next up, alter the statement that checks if the witch has a familiar:

if let familiar = familiar {

…to instead throw a .noFamiliar error from another guard statement:

guard let familiar = familiar else {
  throw ChangoSpellError.noFamiliar
}

Ignore any errors that occur for the moment, as they will disappear with your next code change.

Handling Toad Errors

On the next line, the code returns the existing toad if the Witch tries to cast the turnFamiliarIntoToad() spell on her unsuspecting amphibian, but an explicit error would better inform her of the mistake. Change the following:

if let toad = familiar as? Toad {
  return toad
}

…to the following:

if familiar is Toad {
  throw ChangoSpellError.familiarAlreadyAToad
}

Note the change from as? to is lets you more succinctly check for conformance to the protocol without necessarily needing to use the result. The is keyword can also be used for type comparison in a more general fashion. If you’re interested in learning more about is and as, check out the type casting section of The Swift Programming Language.

Move everything inside the else clause outside of the else clause, and delete the else. It’s no longer necessary!

Handling Spell Errors

Finally, the hasSpell(_ type:) call ensures that the Witch has the appropriate spell in her spellbook. Change the code below:

if hasSpell(ofType: .prestoChango) {
  if let name = familiar.name {
    return Toad(name: name)
  }
}

…to the following:

guard hasSpell(ofType: .prestoChango) else {
  throw ChangoSpellError.spellNotKnownToWitch
}

guard let name = familiar.name else {
  let reason = "Familiar doesn’t have a name."
  throw ChangoSpellError.spellFailed(reason: reason)
}

return Toad(name: name)

And now you can remove the final line of code which was a fail-safe. Remove this line:

return Toad(name: "New Toad")

You now have the following clean and tidy method, ready for use. I’ve provided a few additional comments to the code below, to further explain what the method is doing:

func turnFamiliarIntoToad() throws -> Toad {

  // When have you ever seen a Witch perform a spell without her magical hat on ? :]
  guard let hat = hat, hat.isMagical else {
    throw ChangoSpellError.hatMissingOrNotMagical
  }

  // Check if witch has a familiar
  guard let familiar = familiar else {
    throw ChangoSpellError.noFamiliar
  }

  // Check if familiar is already a toad - if so, why are you casting the spell?
  if familiar is Toad {
    throw ChangoSpellError.familiarAlreadyAToad
  }
  guard hasSpell(ofType: .prestoChango) else {
    throw ChangoSpellError.spellNotKnownToWitch
  }

  // Check if the familiar has a name
  guard let name = familiar.name else {
    let reason = "Familiar doesn’t have a name."
    throw ChangoSpellError.spellFailed(reason: reason)
  }

  // It all checks out! Return a toad with the same name as the witch's familiar
  return Toad(name: name)
}

You could have returned an optional from turnFamiliarIntoToad() to indicate that “something went wrong while this spell was being performed”, but using custom errors more clearly expresses the error states and lets you react to them accordingly.

What Else Are Custom Errors Good For?

Now that you have a method to throw custom Swift errors, you need to handle them. The standard mechanism for doing this is called the do-catch statement, which is similar to try-catch mechanisms found in other languages such as Java.

Add the following code to the bottom of your playground:

func exampleOne() {
  print("") // Add an empty line in the debug area

  // 1
  let salem = Cat(name: "Salem Saberhagen")
  salem.speak()

  // 2
  let witchOne = Witch(name: "Sabrina", familiar: salem)
  do {
    // 3
    try witchOne.turnFamiliarIntoToad()
  }
  // 4
  catch let error as ChangoSpellError {
    handle(spellError: error)
  }
  // 5
  catch {
    print("Something went wrong, are you feeling OK?")
  }
}

Here’s what that function does:

  1. Create the familiar for this witch. It’s a cat called Salem.
  2. Create the witch, called Sabrina.
  3. Attempt to turn the feline into a toad.
  4. Catch a ChangoSpellError error and handle the error appropriately.
  5. Finally, catch all other errors and print out a nice message.

After you add the above, you’ll see a compiler error – time to fix that.

handle(spellError:) has not yet been defined, so add the following code above the exampleOne() function definition:

func handle(spellError error: ChangoSpellError) {
  let prefix = "Spell Failed."
  switch error {
    case .hatMissingOrNotMagical:
      print("\(prefix) Did you forget your hat, or does it need its batteries charged?")

    case .familiarAlreadyAToad:
      print("\(prefix) Why are you trying to change a Toad into a Toad?")

    default:
      print(prefix)
  }
}

Finally, run the code by adding the following to the bottom of your playground:

exampleOne()

Reveal the Debug console by clicking the up arrow icon in the bottom left hand corner of the Xcode workspace so you can see the output from your playground:

Expand Debug Area

Catching Errors

Below is a brief discussion of each of language feature used in the above code snippet.

catch

You can use pattern matching in Swift to handle specific errors or group themes of error types together.

The code above demonstrates several uses of catch: one where you catch a specific ChangoSpell error, and one that handles the remaining error cases.

try

You use try in conjunction with do-catch statements to clearly indicate which line or section of code may throw errors.

You can use try in several different ways:

  • try: standard usage within a clear and immediate do-catch statement. This is used above.
  • try?: handle an error by essentially ignoring it; if an error is thrown, the result of the statement will be nil.
  • try!: similar to the syntax used for force-unwrapping, this prefix creates the expectation that, in theory, a statement could throw an error – but in practice the error condition will never occur. try! can be used for actions such as loading files, where you are certain the required media exists. Like force-unwrap, this construct should be used carefully.

Time to check out a try? statement in action. Cut and paste the following code into the bottom of your playground:

func exampleTwo() {
  print("") // Add an empty line in the debug area
    
  let toad = Toad(name: "Mr. Toad")
  toad.speak()
    
  let hat = Hat()
  let witchTwo = Witch(name: "Elphaba", familiar: toad, hat: hat)
    
  print("") // Add an empty line in the debug area
    
  let newToad = try? witchTwo.turnFamiliarIntoToad()
  if newToad != nil { // Same logic as: if let _ = newToad
    print("Successfully changed familiar into toad.")
  }
  else {
    print("Spell failed.")
  }
}

Notice the difference with exampleOne. Here you don’t care about the output of the particular error, but still capture the fact that one occurred. The Toad was not created, so the value of newToad is nil.

Gemma Barlow

Contributors

Gemma Barlow

Author

Over 300 content creators. Join our team.