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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Magical Error Handling in Swift
30 mins
- Getting Started
- Why Should I Care About Error Handling?
- Avoiding Swift Errors Using nil
- Failable Initializers
- Guard Statements
- Avoiding Errors with Custom Handling
- Refactoring to Use Swift Errors
- Handling Hat Errors
- Handling Familiar Errors
- Handling Toad Errors
- Handling Spell Errors
- What Else Are Custom Errors Good For?
- Catching Errors
- Propagating Errors
- Manipulating Error Handling Behavior
- More Fun with Errors
- The Future of Error Handling in Swift
- Where To Go From Here?
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:
- Create the familiar for this witch. It’s a cat called Salem.
- Create the witch, called Sabrina.
- Attempt to turn the feline into a toad.
- Catch a
ChangoSpellError
error and handle the error appropriately. - 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:
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 immediatedo-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 benil
. -
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
.