Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4384

Magical Error Handling in Swift

$
0
0

Swift Error Handling is Magical

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!

Note: This tutorial assumes you’re familiar with Swift 2 syntax – particularly enumerations and optionals. If you need a refresher on these concepts, start with the What’s New in Swift 2 post by Greg Heo.

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:

Screen Shot 2016-04-15 at 6.45.31 PM

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).

Oh Really?

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.

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.

Owl

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 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

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

The Swift Programming Language (Swift 2.2)

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.

Pyramid of Doom

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.
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 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
}
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 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:

  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.

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:

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, one of which is used above:

  • 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, 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.

“…we believe that we can extend our current model to support untyped propagation for universal errors. Doing this well, and in particular doing it without completely sacrificing code size and performance, will take a significant amount of planning and insight. For this reason, it is considered well out of scope for Swift 2.0.”

– 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.

Excellent

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:

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.


Viewing all articles
Browse latest Browse all 4384

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>