Error handling in Swift has come a long way since the patterns in Swift 1 that were inspired by Objective-C. Major improvements in Swift 2 make the experience of handling unexpected states and conditions in your application more straightforward.
Just like other common programming languages, preferred error handling techniques in Swift can vary, depending upon the type of error encountered, and the overall architecture of your app.
This tutorial will take you through a magical example involving wizards, witches, bats and toads to illustrate how best to deal with common failure scenarios. You’ll also look at how to upgrade error handling from projects written in earlier versions of Swift and, finally, gaze into your crystal ball at the possible future of error handling in Swift!
Time to dive straight in (from the the cauldron into the fire!) and discover the various charms of error handling in Swift 2!
Getting Started
There are two starter playgrounds for this tutorial, one for each section. Download Avoiding Errors with nil – Starter.playground and Avoiding Errors with Custom Handling – Starter.playground playgrounds.
Open up the Errors with nil starter playground in Xcode.
Read through the code and you’ll see several classes, structs and enums that hold the magic for this tutorial.
Take note the following parts of the code:
protocol MagicalTutorialObject { var avatar: String { get } } |
This protocol is applied to all classes and structs used throughout the tutorial to provide a visual representation of each object that can be printed to the console.
enum MagicWords: String { case Abracadbra = "abracadabra" case Alakazam = "alakazam" case HocusPocus = "hocus pocus" case PrestoChango = "presto chango" } |
This enumeration denotes magic words that can be used to create a Spell
.
struct Spell: MagicalTutorialObject { var magicWords: MagicWords = .Abracadbra var avatar = "*" } |
This is the basic building block for a Spell
. By default, you initialize its magic words to Abracadabra
.
Now that you’re acquainted with the basics of this supernatural world, you’re ready to start casting some spells.
Why Should I Care About Error Handling?
“Error handling is the art of failing gracefully.”
–Swift Apprentice, Chapter 21 (Error Handing)
Good error handling enhances the experience for end users as well as software maintainers by making it easier to identify issues, their causes and their associated severity. The more specific the error handling is throughout the code, the easier the issues are to diagnose. Error handling also lets systems fail in an appropriate way so as not to frustrate or upset users.
But errors don’t always need to be handled. When they don’t, language features let you avoid certain classes of errors altogether. As a general rule, if you can avoid the possibility of an error, take that design path. If you can’t avoid a potential error condition, then explicit handling is your next best option.
Avoiding Swift Errors Using nil
Since Swift has elegant optional-handling capabilities, you can completely avoid the error condition where you expect a value, but no value is provided. As a clever programmer, you can manipulate this feature to intentionally return nil
in an error condition. This approach works best where you should take no action if you reach an error state; i.e., you choose inaction over emergency action.
Two typical examples of avoiding Swift errors using nil
are failable initializers and guard statements.
Failable Initializers
Failable initializers prevent the creation of an object unless sufficient information has been provided. Prior to Swift 2 (and in other languages), this functionality was typically achieved via the Factory Method Pattern.
An example of this pattern in Swift can be seen in createWithMagicWords
:
static func createWithMagicWords(words: String) -> Spell? { if let incantation = MagicWords(rawValue: words) { var spell = Spell() spell.magicWords = incantation return spell } else { return nil } } |
The above initializer tries to create a spell using magic words provided, but if the words are not magical you return nil
instead.
Inspect the creation of the spells at the very bottom of this tutorial to see this behavior in action:
While first
successfully creates a spell using the magic words "abracadabra"
, "ascendio"
doesn’t have the same effect, and the return value of second
is nil
. (Hey, witches can’t win all the time).
Factory methods are an old-school programming style. There are better ways to achieve the same thing in Swift. You’ll update the Spell
extension to use a failable initializer instead of a factory method.
Delete createWithMagicWords(_:)
and replace it with the following:
init?(words: String) { if let incantation = MagicWords(rawValue: words) { self.magicWords = incantation } else { return nil } } |
Here you’ve simplified the code by not explicitly creating and return the Spell
object.
The lines that assign first
and second
now throw compiler errors:
let first = Spell.createWithMagicWords("abracadabra") let second = Spell.createWithMagicWords("ascendio") |
You’ll need to change these to use the new initializer. Replace the lines above with the following:
let first = Spell(words: "abracadabra") let second = Spell(words: "ascendio") |
After this, all errors should be fixed and the playground should compile without error. With this change your code is definitely tidier – but you can do even better! :]
Guard Statements
guard
is a quick way to assert that something is true: for example, if a value is > 0, or if a conditional can be unwrapped. You can then execute a block of code if the check fails.
guard
was introduced in Swift 2 and is typically used to (bubble, toil and trouble) bubble-up error handling through the call stack, where the error will eventually be handled. Guard statements allow early exit from a function or method; this makes it more clear which conditions need to be present for the rest of the processing logic to run.
To clean up Spell
‘s failable initializer further, edit it as shown below to use guard
:
init?(words: String) { guard let incantation = MagicWords(rawValue: words) else { return nil } self.magicWords = incantation } |
With this change, there’s no need need for an else
clause on a separate line and and the failure case is more evident as it’s now at the top of the intializer. Also, the “golden path” is least indented. The “golden path” is the path of execution that happens when everything goes as expected, i.e. no error. Being least indented means that it is easy to read.
Note that the values of first
and second
Spell
constants haven’t changed, but the code is more more streamlined.
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 intriciate 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: MagicalTutorialObject { var magicWords: MagicWords = .Abracadbra var avatar = "*" 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 MagicalTutorialObject
protocol, and a second failable initializer, which has been added for convenience.
protocol Familiar: MagicalTutorialObject { var noise: String { get } var name: String? { get set } init() init(name: String?) } |
The Familiar
protocol will be applied to various animals (such as bats and toads) further down in the playground.
This clearly isn’t Hedwig, but still cute nonetheless, no?
struct Witch: MagicalBeing { 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 { // Check if familiar is already a toad - no magic required return toad } else { if hasSpellOfType(.PrestoChango) { if let name = familiar.name { return Toad(name: name) } } } } } } return Toad(name: "New Toad") // This is an entirely new Toad. } func hasSpellOfType(type: MagicWords) -> Bool { // Check if witch currently has appropriate spell in their spellbook return spells.contains { $0.magicWords == type } } } |
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 inspells
, which is an array ofSpell
objects. - A
Witch
seems to have a penchant for turning her familiar into a toad via the use of the.PrestoChango
spell, withinturnFamiliarIntoToad()
.
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
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 significant cognitive effort.
Guard statements, as you’ve seen earlier, and multiple optional binding 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
ErrorType
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: ErrorType { case HatMissingOrNotMagical case NoFamiliar case FamiliarAlreadyAToad case SpellFailed(reason: String) case SpellNotKnownToWitch } |
Note two things about ChangoSpellError
:
- It conforms to the
ErrorType
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.
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 MagicalBeing
protocol:
protocol MagicalBeing: MagicalTutorialObject { 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 } |
}
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 more clear to group the checks together on a single line for clarity. As such, change the first guard
statement to match the following:
guard let hat = hat where 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.
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, you return 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 hasSpellOfType(type:)
call ensures that the Witch has the appropriate spell in her spellbook. Change the code below:
if hasSpellOfType(.PrestoChango) { if let toad = f as? Toad { return toad } } |
…to the following:
guard hasSpellOfType(.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 where 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 hasSpellOfType(.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 simply 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 { handleSpellError(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.
handleSpellError()
has not yet been defined, so add the following code above the exampleOne()
function definition:
func handleSpellError(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, one of which is used above:
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, 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) 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
.
Propagating Errors
throws
The throws
keyword is required in Swift if a function or method throws an error. Thrown errors are automatically propagated up the call stack, but letting errors bubble too far from their source is considered bad practice. Significant propagation of errors throughout a codebase increases the likelihood errors will escape appropriate handling, so throws
is a mandate to ensure propagation is documented in code – and remains evident to the coder.
rethrows
All examples you’ve seen so far have used throws
, but what about its counterpart rethrows
?
rethrows
tells the compiler that this function will only throw an error when its function parameter throws an error. A quick and magical example can be found below (no need to add this to the playground):
func doSomethingMagical(magicalOperation: () throws -> MagicalResult) rethrows -> MagicalResult { return try magicalOperation() } |
Here doSomethingMagical(_:)
will only throw an error if the magicalOperation
provided to the function throws one. If it succeeds, it returns a MagicalResult
instead.
Manipulating Error Handling Behavior
defer
Although auto-propagation will serve you well in most cases, there are situations where you might want to manipulate the behavior of your application as an error travels up the call stack.
The defer
statement is a mechanism that permits a ‘cleanup’ action to be performed whenever the current scope is exited, such as when a method or function returns. It’s useful for managing resources that need to be tidied up whether or not the action was successful, and so becomes especially useful in an error handling context.
To see this in action, add the following method to the Witch
structure:
func speak() { defer { print("*cackles*") } print("Hello my pretties.") } |
Add the following code to the bottom of the playground:
func exampleThree() { print("") // Add an empty line in the debug area let witchThree = Witch(name: "Hermione", familiar: nil, hat: nil) witchThree.speak() } exampleThree() |
In the debug console, you should see the witch cackle after everything she says.
Interestingly, defer
statements are executed in the opposite order in which they are written.
Add a second defer
statement to speak()
so that a Witch screeches, then cackles after everything she says:
func speak() { defer { print("*cackles*") } defer { print("*screeches*") } print("Hello my pretties.") } |
Did the statements print in the order you expected? Ah, the magic of defer
!
More Fun with Errors
The inclusion of the above statements in Swift brings the language into line with many other popular languages and separates Swift from the NSError
-based approach found in Objective-C. Objective-C errors are, for the most part, directly translated, and the static analyzer in the compiler is excellent for helping you with which errors you need to catch, and when.
Although the do-catch
and related features have significant overhead in other languages, in Swift they are treated like any other statement. This ensures they remain efficient – and effective.
But just because you can create custom errors and throw them around, doesn’t necessarily mean that you should. You really should develop guidelines regarding when to throw and catch errors for each project you undertake. I’d recommend the following:
- Ensure error types are clearly named across your codebase.
- Use optionals where a single error state exists.
- Use custom errors where more than one error state exists.
- Don’t allow an error to propagate too far from its source.
The Future of Error Handling in Swift
A couple of ideas for advanced error handling are floating around various Swift forums. One of the most-discussed concepts is untyped propagation.
– from Swift 2.x Error Handling
Whether you enjoy the idea of a major error handling change in Swift 3, or are happy with where things are today, it’s nice to know that clean error handling is being actively discussed and improved as the language develops.
Where To Go From Here?
You can download the finished set of playgrounds here for this tutorial.
For further reading, I recommend the following articles, some of which have already been referenced throughout this tutorial:
- Swift Apprentice, Chapter 21 – Error Handling
- Failable Initializers
- Factory Method Pattern
- Pyramid of Doom
If you’re keen to see what may lie ahead in Swift 3, I recommend reading through the currently open proposals; see Swift Language Proposals for further details. If you’re feeling adventurous, why not submit your own? :]
Hopefully by now that you’ve been truly enchanted by error handling in Swift. If you have any questions or comments on this tutorial, please join the forum discussion below!
The post Magical Error Handling in Swift appeared first on Ray Wenderlich.