Handling Errors the Swift Way

One thing we haven’t considered is what to do when things go wrong. So far, our only defensive tactic has been the cautious use of if let to avoid crashing when we unwrap optionals that turn out to be nil. But there’s more to robust coding practices than that.

Swift 2 introduced a new error-handling paradigm that is supported by many of the iOS frameworks. It will be familiar to readers who’ve seen try-catch--style semantics in other languages, but its differences are important to understand: these aren’t your father’s java.lang.Exceptions.

In Swift, methods (including initializers) can indicate that they signal errors by including the throws keyword. To call code that may throw, we need to do two things:

  1. Wrap all related code in a do-catch block, where the catch will pull out and handle any thrown object.

  2. Explicitly put the keyword try immediately before each method or initializer that can throw. Of course, the compiler could figure it out for us; putting the onus on the coder is meant as a means of annotating the code by explicitly calling attention to parts of the code that can produce errors.

Let’s try an example. The iOS frameworks have a number of APIs that throw errors in Swift. In many cases, these were implemented in Objective-C with an “in-out” system where a caller would provide a pointer to an NSError object. The caller would send in nil for this pointer, and check its value after the method returned. If it was now a non-nil pointer to an NSError, it meant that an error had occurred. Any such “in-out” APIs are converted automatically to the throws idiom in Swift, so don’t let the documentation scare you if you see those Objective-C asterisks (**NSError always freaked us out).

As an example, there’s a class called Data that wraps an in-memory data buffer of any size. It can be populated with the contents of any URL with the init(contentsOf:options:) initializer, but…it’s marked with throws, which means if we use it, we have to deal with a possible error. And that makes sense, of course: what should it do if your URL is nonsense or if there’s a network error? Throwing an error describing the problem at least gives us a chance of recovering or telling the user what happened.

Start a new playground called ErrorHandlingPlayground, and delete the “Hello, playground” line. It’s been a long chapter, so we’ll make this short:

1: if​ ​let​ myURL = ​URL​(string: ​"http://apple.com/"​) {
2: do​ {
3: let​ myData = ​try​ ​Data​ (contentsOf: myURL, options: [])
4: let​ myString = ​String​(data: myData, encoding: .utf8)
5:  } ​catch​ ​let​ nserror ​as​ ​NSError​ {
6:  print (​"NSError: ​​(​nserror​)​​"​)
7:  } ​catch​ {
8:  print (​"No idea what happened there: ​​(​error​)​​"​)
9:  }
10: }

Line 3 tries to create the Data from the provided myURL. This initializer throws, so the initializer itself needs to have the try keyword right before it; when a method or function throws, the try will be at the beginning of the line.

If we successfully download the Data, we use it on line 4, where it’s used to create a String from the data, and a hint about what text encoding the data uses (UTF-8 is often a good bet). This line needs to be inside the do-catch only because it needs to have the myData in scope.

So, when this works, we’ll see the Data and String represented over in the result area: the data will be clusters of hexadecimal digits, and the string will be the raw HTML.

Now let’s get ourselves into the error handling. We’ll do that by mangling the URL string. A simple way to do this is to change the URL scheme from http to some nonsense like foo. Do this, and the evaluation pane will go blank. Instead, down in the debug console, we’ll see an error message:

 NSError: Error Domain=NSCocoaErrorDomain Code=256 "The file “foo” couldn’t
 be opened." UserInfo={NSURL=foo}

This is coming from the block on line 5 that catches thrown NSErrors, the errors from the older Cocoa frameworks that underlie much of iOS. The later catch on line 7 catches anything, although nothing in our code is declared as throwing something other than NSError, so it will never be reached and is shown only for demonstration.

So, in a nutshell, that’s Swift error-handling: if something declares that it throws, wrap the call in a do-catch, decorate all calls that can throw with try, and then catch whatever was thrown, using the let as construct to pick apart the type that was thrown.

Oh, and let’s please be sure to do more to recover than just logging an error to the Xcode console. In real life, we would want to tell the user what happened, or maybe automatically retry, or something.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset