nil}2018-09-04T00:00:00-08:00{"modified_time"=>nil}" />
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.
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 throw
s 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.
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 |
---|---|---|---|
nil | nil | nil | impossible; should always have a URLResponse |
nil | nil | present | impossible; should always have a URLResponse |
nil | present | nil | impossible; should always have only one of either a Data or an Error |
nil | present | present | handle the Error (including passing it back to callers) |
present | nil | nil | impossible; should always have a URLResponse |
present | nil | present | impossible; should always have a URLResponse |
present | present | nil | pass the Data or a deserialized object or data structure back to callers |
present | present | present | impossible; 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 let
s, and you only have to do it once. The problem lies in functions that don’t rethrow errors thrown in their closure parameters…
If you’ve ever map
ped 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.
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.
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:
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>
.
See: this blog post
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:
success/failure
case names in each switch
.switch
boilerplate easier to cognitively filter out than do-try-catch
boilerplate.do-try-catch
imposes a specific order on your code statements and blocks. You must write the statements that depend on the result of the throwing call after that call, and the error handling must follow that in a catch block. You can’t write a catch block anywhere else. With switch-failure-success
, you can write the failure
case first, similar to an early exit with a guard
(my preference), but you can also put the success
case first. switch
imposes no ordering on its case labels, so even though this is a small, pedantic nitpick, you must expend a little cognitive overhead each time to decide which case to write first. And if you have anything less than perfect memory and willpower, you will eventually swap the ordering in your code, introducing inconsistency.do
block that calls multiple throw
ing functions at the same scope level. If each of those instead returned a Result
, they must be nested inside each other’s switch
blocks, or use higher-order functions like map
to recompose the results (further overloading the meaning of those function names, and requiring you to remember to use flatMap
with Result
as with Optional
, and not compactMap
as with Sequence
).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 Error
s, 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!.