Road to Result
When Swift was introduced as another tool at the disposal of developers as an alternative to Objective-C, one of most prominent features of Swift was its emphasis on type safety and its heavy leverage of the
Optional brought with very healthy programming practices and it also forced developers to deal with the possibility of a value being
nil or have a compile time error staring them in the face, this is a big improvement over the Objective-C ways of thinking of dealing with it at run-time.
This has done wonders in reducing run-time errors but by no means is
Optional a silver bullet.
There are many reasons why a value could be
nil and if the context is obvious enough
Optional might suffice, but in most other cases more context is needed and unfortunately
Optional can not provide this, at best
nil can symbol that something went wrong.
This is where
Result comes to save the day!
Evolution of the Result type
Starting with Swift 5, the type
Result is baked into Swift's standard library.
Result is very simple, you could easily roll your own:
Thats all there is to it, just a simple
Either data type, i.e either A or B but never the possibility of both at the same time kinda like the concept of super position:
The 'B' in the case of Result is specific, it must be a type that conforms to
Result micro frameworks such as
antitypical/Result have been around almost as long as Swift it's self has.
The usage of
Result in Swift code is nothing new but just how
antitypical/Result provided a common definition of the
Result type that would allow multiple libraries to share a single definition making code integration less painful, the
Result type provided by the Swift standard library allows code to share a now universal result type, one that is on every single installation of swift from here on out.
Result is in the standard library, an "official" definition exists and the swift community would benefit by leaving third-party
Result types behind and simply leveraging Swift 5's
Result is commonly returned from a function that can fail.
Take for example a fictional function that registers a master passphrase for a password manager app.
The rules are simple, the password must be ten characters or more and composed of upper and lower case characters.
When an error occurs we simply return
nil which is not very descriptive and forces us to use a one-size-fits-all message like "Password not vaild, try again" but its better than using something like magic symbols like an empty string to convey an error.
Of course the return value could be a tuple with an error or error message AND a success message like this:
but then this code could express a situation that should be impossible, a kind of "Superposition" case that only Schrodinger would appreciate.
I don't like the Schrodinger's Cat concept because what kind of monster would put a cat in danger?
Superposition is cool but it has no place in a modern typed programming languages (when avoidable) so lets stick to a single
Result return value and not kill any cats.
Calling the above code with different failing invalid password strings for different reasons all would produce the same
nil return value, not very descriptive and this leaves much to be desired. All we can do as a consumer of this function is display a generic message to the user of our app like "Password phrase invalid, please try again."
Well, we know that
Result's failure type must conform to error, and an
enum is a simple and clean way of expressing different distinct states so lets go ahead and fill that out:
OK! Now that thats out of the way lets revisit what our password checking function would look like now that it returns a result with meaningful failure information.
Now calling our function would produce the following output:
And finally we can get around to writing our error handling code a little more concise and clearly.
Writing the logic to be either success or failure and now there is no chance of "superposition" or some other undefined state where they are both
Features of the Result type
Result has a constructor to create a Result from code that throws.
Result has also two methods for transforming success values:
flatMap and two methods for transforming errors:
Replacing code that
One handy feature of Result is it's ability to wrap code that throws errors.
This has a few benefits but one obvious one is that instead of catching the error and dealing with it on the spot, the output of
Result can be saved and if its an error it can be dealt with at a later time.
Many error handling examples deal with network code so I will try to be less cliche and use an example of dealing with exceptions that arise from parsing, namely
Codable failing to decode JSON.
Given the above
CardboardBox structure it could be initialized with the following valid JSON:
Thats quite a bit of code just to handle one possible exception, more than that we are forced to handle the event of an error in the
catch block at the moment an exception is thrown.
I personally find this lacking but fortunately there is good news:
Result has an initializer that takes a closure that throws exceptions and automatically wraps it in a handy Result type.
The example below has intentionally invalid JSON to demonstrate error handling with the
The property "flavor" should be a
String but here we use an Integer (42, the answer to the meaning of life.)
This will cause
JSONDecoder to fail.
Result's closure constructor to wrap the throwing code and now we can deal with the end result in a very readable switch statement.
One of the big benefits of this approach is that the result is saved and stored away for later in
myBoxResult, it can be dealt with when it is convenient for the programmer which is very important for dealing with asynchronous code.
Result can be transformed using
mapto return a new
Result containing a different value in the
success case. If the
Result ended up in an Error the closure provided to map is ignored and the error is passed through.
Below is an example of calling map on a Result of type
Result<Int, CactusError> and transforming it to type
flatMap behaves much the same as
map except that it unwraps the success value and after the closure evaluates, repackages the result in a new
This is done to avoid having
Results inside of
Result<Result<String, Error>, Error>
mapError and flatMapError
These two functions behave exactly the same way as
flatMap do except that they only execute the closure passed to them when the
Result evaluates to
Please feel free to add any changes, suggest fixes, edits or new code examples that you feel would improve this article. Pull requests are always welcome, if you feel inclined please send it to this site's blog repository:
There are many other great mini-tutorials out there that will pop up in your search results but in addition to reading those I highly suggest you take a look at the Swift Evolution Proposal SE-235: Add result
Happy Trails! 🤠