Jeremy W. Sherman

stay a while, and listen

Using Swift Throws with Completion Callbacks

Swift 2 introduced the notion of throwing and propagating NSError values.

It works pretty well in a linear, synchronous workflow, but at first glance, it doesn’t appear to address the common case of completion callbacks.

Consider NSURLSession.dataTaskWithURL(_:completionHandler:). Swift 2 bridges this in like so:

1
2
3
func dataTaskWithURL(url: NSURL,
    completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void)
    -> NSURLSessionDataTask?

Note how, in the completion handler closure, you still have to do Ye Olde Check Data Then Check Error dance. Yawn.

There’s a straightforward way to transform this into throws-land, though. Just think: What sort of thing can throw? A function call.

So, let’s use our functions, and rewrite this to:

1
2
3
4
typealias DataTaskResult = () throws -> (NSData, NSURLResponse)
func dataTaskWithURL(url: NSURL,
    completionHandler: DataTaskResult -> Void)
    -> NSURLSessionDataTask?

The completion handler is not marked as @rethrows, so it has to handle any error. Extracting the result or error is then done in the completion handler like so:

1
2
3
4
5
6
7
8
{ result: DataTaskResult in
    do {
        let data, response = try result()
        /* work with data and response */
    } catch {
        /* you got yourself an error! */
    }
}

This straightforward transformation preserves Swift 2’s directing attitude towards error-handling, while freeing users from having to remember the protocol for working with NSErrors.

It’s unfortunate we can’t ourselves apply this to Apple’s code. We’ll just have to continue to type through the error-prone, manual procedure for working with NSErrors when working with their APIs.

We needn’t continue to do so with our own, though: if you’re going to adopt throws, go whole-hog, and throwify your entire API, both synchronous and asynchronous.