Notes from WWDC 2015: Failing Gracefully: Swift 2.0 Error Handling
Experiencing WWDC's information dump resembles what it's like to sip daintily from a firehose. It's hard to process and absorb the facts and suggestions that are firing at you from every side. I've forced myself to sneak away from the party to spend some serious time getting up to speed with the newly updated Swift language.
It's only been two days since Apple launched 2.0, and I'm already appreciating the way this update has addressed developer concerns. Swift 2.0 introduces both new constructs such as for-in-where (this lets you filter which items are processed) and updated syntax like sortInPlace (replacing the confusing sort and sorted calls from 1.x).
The result is a language that looks and feels very much like Swift 1.x but that offers more comparatively developer-focused features. The 2.0 update, which can be called "Version n – 1.5" or even more generously "Version n - 1", moves the language a lot closer to what people expect for a language launch. The compiler diagnostics in 2.0 read a lot less like "random fortune cookie paper slips" (although they still can be maddening). It's a great step forward.
Error Handling
The biggest Swift 2.0 changes involve error handling. When I say "error handling", what I mean is support for Apple's vast Cocoa/Cocoa touch ecosystem. Cocoa has a particular way of working. APIs return a usable value or some fail semaphore such as false or nil. There's often an error parameter that's simultaneously populated. At each step, you check whether a call has failed. If so, you print an error message and return early from the method or function.
Objective-C fits well into this traditional Cocoa paradigm. Code follows a smooth linear path of calls, tests, and returns. Swift, on the other hand, has struggled to match this approach. Type safety doesn't marry easily into return polymorphism and side effects. The 2.0 update attempts to address these issues.
1.2 Optionals
Here is some 1.2 code that I'm currently refactoring in Swift 2.0. I starting to develop this routine this past Sunday right before WWDC. It sends a request to Bit.ly to shorten a URL and displays either the shortened version or an error.
public struct Bitly { public static func shorten(urlString : String) -> String? { let endPoint = apiEndpoint + urlString.urlEscapedRepresentation var error : NSError? = nil if let url = NSURL(string:endPoint) { let request = NSURLRequest(URL: url) if let data = NSURLConnection.sendSynchronousRequest( request, returningResponse: nil, error: &error), node = XMLParser.parseXML(data), dataNode = node.childWithKey("data"), urlNode = dataNode.childWithKey("url"), leafValue = urlNode.cleanLeafValue { return leafValue } } println("Error encountered when placing synchronous request") if let error = error {println(error)} return nil } }
This code demonstrates some of the struggles involved when working with Cocoa. The URL initializer can fail. The error instance is only used in the synchronous request API. The only way to error gracefully is to return an optional, with error handling a side effect of the nil value.
While the approach I was working on was a step up from pre-1.2 Swift, the convoluted if let cascade remained convoluted and hard to use. The error container is only used with one of the calls.
Swift 2.0 Throws
Compare the 1.2 version with the following code. This is my attempt to refactor the routine to 2.0. While I expect to keep working on this function, it already shows significant improvements over the previous language implementation. The function declares the throws keyword just after the parameter list, indicating that it works in Swift 2.0's new error-handling system.
public struct Bitly { public static func shorten(urlString : String) throws -> String { do { let endPoint = apiEndpoint + urlString.urlEscapedRepresentation guard let url = NSURL(string: endPoint) else {throw BuildError("Could not construct URL")} let request = NSURLRequest(URL:url) let data = try NSURLConnection.sendSynchronousRequest(request, returningResponse: nil) if let node = XMLParser.parseXML(data), dataNode = node.childWithKey("data"), urlNode = dataNode.childWithKey("url"), leafValue = urlNode.cleanLeafValue { return leafValue } } catch {throw error} throw BuildError("Unable to shorten URL \(urlString)") } }
Eliminating Optionals
My new version doesn't return an optional. When the routine succeeds, it returns a ready-to-use String, not a String?. This is a hallmark of Swift 2.0. You use far fewer optionals. While this code contains an if-let cascade, it's used only for my custom XMLParser struct, which I have yet to refactor in any major fashion.
Two calls that escape if-let include the NSURL constructor and the synchronous request.
The constructor now uses Swift 2.0's guard syntax. NSURL instances are fallible. They can return nil or a URL value. Using guard guarantees that any nil results exit the current scope, in this example by throwing an error. That means the guard let assignment is guaranteed to be non nil. The new URL value can be used directly without tests or unwrapping. It is no longer an optional.
Guard statements can also be cascaded as you do with if-let. This creates a simple way to establish variables at the start of your methods and functions, with a single error-handling block should any of your variable pre-conditions fail.
Eliminating Error Variables
The following is the sendSynchronousRequest method declaration for 1.2.
class func sendSynchronousRequest( request: NSURLRequest, returningResponse response: AutoreleasingUnsafeMutablePointer, error: NSErrorPointer) -> NSData?
Compare this with the same method, as it is declared in 2.0.
class func sendSynchronousRequest( request: NSURLRequest, returningResponse response: AutoreleasingUnsafeMutablePointer) throws -> NSData
In the 2.0 update, the NSError variable disappeared from my primary scope. It also left the sendSynchronousRequest method, which now takes only two parameters. Updated Cocoa annotation means this request is now explicitly fallible and can throw an error. The throws keyword declares this behavior.
Importantly, the return type is now NSData and not NSData?. Error-handling ensures that there is only one success path for this call, and that path involves a non-optional result. This bypasses old-Swift if-let and Objective C if-not-nil checks, simplifying code and enabling errors to move directly to handlers.
My updated code uses a catch clause not so much because it needs one but to show where you might catch an error to enable clean wrap-up.
Speaking of clean-up, Swift 2.0 adds a defer statement. Defer adds commands that delay execution until the current code block prepares to exit. You can close files, deallocate resources, and perform any other clean-up that might otherwise need handling from early exit fail conditions in a single call.
Each defer block executes at the end of a scope whether or not errors were encountered. They're stored in a stack so they execute in reverse order of their declarations.
Wrap-up
Swift 2.0's new error handling features enable you to create code that focuses more on successful calls and less on endless error conditions. It allows you to restructure your methods and functions to handle a one-true-path of success instead of many-failing-paths of failure. It's an exciting development for the language and a promising one for developers looking to see how this language will fit into their daily workflow.