nil}2018-09-04T00:00:00-08:00{"modified_time"=>nil}" />

Blog

do try Antitypical's Result to catch Async Errors in Swift
Andrew McKnight – 4 September 2018

I’ve previously written about using swift errors vs optionals to indicate errors while fetching results (e.g. no file descriptors available to open a file to read its contents) vs absence of results (e.g. the file was read but it was empty). After publishing, a friend asked if I had heard of the Result library, and if I’d considered using that instead of Swift errors. Indeed, I had used it due to its inclusion in ReactiveSwift/ReactiveCocoa, but had never in my own code or libraries’ API.

What I tried

I took a generic function from my model layer, which inserts a Core Data entity and returns the instance as its own type instead of NSManagedObject, or throws an error if the cast fails. I changed it from throws -> T to -> Result<T, CoreDataError> (typed error semantics is a nice plus for Result). This small change at such a fundamental level precipitated many code changes, and because it’s a function in a framework used by several of my apps, more work lay ahead if I committed to the change. I wound up concluding that I didn’t care to replace all error handling with Result in my own API, but I found it very helpful resolving a particular situation: combinatorial optionality in nonrethrowing function closures.

Combinatorial optionality

Lots of APIs return an optional result and optional error, like NSURLSession.dataTask(with:completionHandler:), whose completion block provides parameters (Data?, URLResponse?, Error?). Because optionals are a compiler feature, you’re forced to deal with scenarios like getting nil for all three parameters. According to NSURLSession’s docs, this can’t happen:

If the request completes successfully, the data parameter of the completion handler block contains the resource data, and the error parameter is nil. If the request fails, the data parameter is nil and the error parameter contain information about the failure. If a response from the server is received, regardless of whether the request completes successfully or fails, the response parameter contains that information.

Nonetheless, you must conceptually handle each case in code:

Data?URLResponse?Error?Behavior
nilnilnilimpossible; should always have a URLResponse
nilnilpresentimpossible; should always have a URLResponse
nilpresentnilimpossible; should always have only one of either a Data or an Error
nilpresentpresenthandle the Error (including passing it back to callers)
presentnilnilimpossible; should always have a URLResponse
presentnilpresentimpossible; should always have a URLResponse
presentpresentnilpass the Data or a deserialized object or data structure back to callers
presentpresentpresentimpossible; should always have only one of either a Data or an Error

Only two of the eight combinations even make sense. I really want union semantics: a way to tell the compiler that a reference can point to either one thing/type or another at a time. Result achieves the same expressibility using Swift’s error throwing itself, along with enums and generics in a sort of Swift hat-trick. Funny enough, it’s the solution I found myself building towards when I sat down to implement my own union-y type specific to my app’s domain, instead of the general-purpose Result (starting from the Swift Language Guide’s recommendation to use enums with associated values. There’s also Either, which generalizes the concept further, but its .left and .right names seemed too generic to me, whereas Result has more specific terminology for the current domain: .success’ and .failure`.

(URLResponse, Result<Data, Error>) exactly transcribes what the docs say into code that’s enforceable by the compiler. Of course, it’d be even easier to have an invariant return type on a function that throws and error. Even then, combinatorial optionality is easier to deal with, via a few early exits from guard lets, and you only have to do it once. The problem lies in functions that don’t rethrow errors thrown in their closure parameters…

Nonrethrowing function closures

If you’ve ever mapped over a Swift Array, you’ve used a function that rethrows Swift errors thrown from its closure parameter’s scope. It’s as simple as declaring a function as rethrows and the closure parameter it accepts as throws. Otherwise, any thrown errors must be handled inside that closure’s scope, and this is enforced by the compiler. (To see more, check out “Rethrowing Functions and Methods” under “Functions” in the Swift language reference’s “Declarations” section.)

Any calls to nonrethrowing functions in a call hierarchy stop thrown error propagation at that point. For instance, imagine a user action results in saving some data to your local cache and then uploading the data over the network, with any errors presented back to the user. A simplified description of the flow of execution through different application layers might look like so: UI -> model -> network, with errors propagating backwards. If all layers had functions declared with throws/rethrows, then we could just throw an error all the way back to the UI layer, where a catch block would handle it and present a dialog to the user. However, because NSURLSession.dataTask accepts a closure and is not declared rethrows, you cannot throw any error in the first place for failures. You must design your call hierarchy to use async patterns (closures, delegates, notifications etc) and pass errors as optional parameters.

My app: CLGeocoder and Core Data

At some point in my app, Core Data entities (returned from the same function I mentioned earlier) are populated with data returned from CLGeocoder.reverseGeocodeLocation(_:completionHandler:). Like NSURLSession, the completionHandler closure passes back multiple optional parameters, in this case ([CLPlacemark]?, Error?). Whereas with plain functions I’d throw an error all the way back from my model/network layers to the UI, now they all call closures passing something like Result<CLLocation, Error>. Result has mostly reduced the difference between propagating errors synchronously and async to one of syntax.

Conclusion

From a reading of Swift docs and evolution and forum posts, it seems that the most likely thing we’ll see is an async/await mechanism that works with throw, and we can break out of the restrictive closures I mentioned (hopefully). Until then, I’ll keep using Result as a way to maintain clean call/error hierarchies that, from time to time, must include that tough breed of closures.

If you’re weighing whether to use Result in your own API, here are the other pros and cons I came up with while experimenting with it:


Pros

Typed Errors

A Swift function may declare that it throws an error, but not which subtypes of Error they may be. You may attempt conditional casts in catch declarations, but you must always fall back to the “default” case of the Error root type as well. Result allows you to specify a particular subtype, such as Result<String, MySwiftErrorSubtype>.

No Combinatorial Optionality

See: this blog post

Cons

Structural Code Changes

Unless you decide at some point to convert the result’s error field to a thrown error, the change from do-try-catch to switch-success-failure permeates throughout your codebase. At first, there was no clear answer on where to make the switch for me, so I just passed the Result all the way to my presentation layer. This changed a lot of code everywhere in my app, although in the end I found it comprehensible and concise.

Changing from a do-try-catch to switch-failure-success has its own tradeoffs:

Pros

Cons

Dependency

Inserting a dependency requiring pervasive changes, that I don’t maintain, and that changes the way I express code execution, is risky. Changes in anything from Swift Errors, stdlib protocols, generics or closures could affect how Result operates, and maybe even necessitate code changes to fix API breakage. However small, there is a nonzero probability that I may one day have to rewrite the code that used Result.


🙏🏻 Enjoy the post? Please help support more like it by buying me a cup of coffee!.

💡 Suggestions, questions, comments? Please submit a PR!.