In a previous article, I talked about error handling in Swift using throw
. But what happens when you deal with asynchronous workflows, where throw
can’t really fit?
What’s wrong with throw
and async?
As a reminder, a function which can fail can use throw
in the following way:
// Define an error type and a throwing function
enum ComputationError: ErrorType { case DivisionByZero }
func inverse(x: Float) throws -> Float {
guard x != 0 else { throw ComputationError.DivisionByZero }
return 1.0/x
}
// Call it
do {
let y = try inverse(5.0)
} catch {
print("Whoops: \(error)")
}
But what happens when the function is asynchronous and will only return a result later, e.g. with a completion block?
func fetchUser(completion: User? /* throws */ -> Void) /* throws */ {
let url = …
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
// if let error = error { throw error } // No can do, fetchUser can't "throw asynchronously"
let user = data.map { User(fromData: $0) }
completion(user)
}.resume()
}
// Call it:
fetchUser() { (user: User?) in
/* do something */
}
How can you throw
in case where the request failed?
- It doesn’t make sense to make the
fetchUser
function itself tothrow
, because this function returns immediately and the network error would only happen later. So, when the error occurs, it would be too late tothrow
an error as the result of thefetchUser
function call itself. - Maybe you can mark the
completion
asthrows
? But the code doing the call to thatcompletion(user)
method is inside thefetchUser
, it’s not at call site. So the one receiving and forced to handle the error would be the code insidefetchUser
itself, not the call site. So that doesn’t solve it either. 😢
Hacking it
One way of working around this limitation is to make the completion
not return a User?
to the caller directly, but a throwing function Void throws -> User
instead, itself returning a User
(let’s call this a UserBuilder
). This way, we can use throw
again.
Then when the completion returns the userBuilder
func, we’ll then need to call try userBuilder()
to access the User
… or have it throw
on error.
enum UserError: ErrorType { case NoData, ParsingError }
struct User {
init(fromData: NSData) throws { /* … */ }
/* … */
}
typealias UserBuilder = Void throws -> User
func fetchUser(completion: UserBuilder -> Void) {
let url = …
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
completion({ UserBuilder in
if let error = error { throw error }
guard let data = data else { throw UserError.NoData }
return try User(fromData: data)
})
}.resume()
}
fetchUser { (userBuilder: UserBuilder) in
do {
let user = try userBuilder()
} catch {
print("Async error while fetching User: \(error)")
}
}
This way, the completion
does not directly return a User
but a function returning a User
… or throwing. And then you have your error handling again.
But, let’s be honest, that’s not the prettiest and most readable solution, having to return a Void throws -> User
instead of returning a User?
directly. So what are our other alternatives?
Introducing Result
Back in Swift 1.0, when throw
didn’t exist yet, people started to handle errors in a functional way. As Swift borrowed a lot of features from the functional programming world, it was logical that people used the Result
pattern in Swift to better handle errors. Here is what Result
looks like1:
enum Result<T> {
case Success(T)
case Failure(ErrorType)
}
The Result
type is actually quite simple: it represents either a success — which has an associated value representing the successful result — or a failure — with an associated error. Perfect to represent operations which can potentially fail.
Then how do we use it? Just build either a Result.Success
or a Result.Failure
and call completion
with the resulting Result
2:
func fetchUser(completion: Result<User> -> Void) {
let url = …
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
if let error = error {
return completion( Result.Failure(error) )
}
guard let data = data else {
return completion( Result.Failure(UserError.NoData) )
}
do {
let user = try User(fromData: data)
completion( Result.Success(user) )
} catch {
completion( Result.Failure(error) )
}
}.resume()
}
Remember monads?
The nice thing about Result
is it can be turned into a Monad. Remember Monads? That simply means that we can add the high-order map
and flatMap
methods on Result
, which will take a closure f: T->U
or f: T->Result<U>
and return a Result<U>
.
If the original Result
was a .Success(let t)
then we apply the closure to that t
and return the resulting f(t)
. If it was a .Failure
then we simply pass the error along:
extension Result {
func map<U>(f: T->U) -> Result<U> {
switch self {
case .Success(let t): return .Success(f(t))
case .Failure(let err): return .Failure(err)
}
}
func flatMap<U>(f: T->Result<U>) -> Result<U> {
switch self {
case .Success(let t): return f(t)
case .Failure(let err): return .Failure(err)
}
}
}
For more information I invite you to re-read my article about Monads, but long story short, this allows us to do stuff like this:
func readFile(file: String) -> Result<NSData> { … }
func toJSON(data: NSData) -> Result<NSDictionary> { … }
func extractUserDict(dict: NSDictionary) -> Result<NSDictionary> { … }
func buildUser(userDict: NSDictionary) -> Result<User> { … }
let userResult = readFile("me.json")
.flatMap(toJSON)
.flatMap(extractUserDict)
.flatMap(buildUser)
What is cool in that expression is that if one of the method (say toJSON
for example) fails and return a .Failure
, then the failure will be propagated to the end without even trying to apply the extractUserDict
and buildUser
methods.
This somehow allows the error to “take a shortcut”: exactly like with do…catch
, you can process all your error cases together in one point at the end of the chain instead of handling errors at each intermediate stage. Isn’t that cool?
From Result
to throw
and back
Problem is, Result
is not built in the Swift standard library, and a lot of functions use throw
to report synchronous errors anyway. Like in practice, to build a User
from a NSDictionary
we might have a init(dict: NSDictionary) throws
constructor instead of a NSDictionary -> Result<User>
function.
How can we mix both those worlds? Easy: let’s extend Result
just for that3!
extension Result {
// Return the value if it's a .Success or throw the error if it's a .Failure
func resolve() throws -> T {
switch self {
case Result.Success(let value): return value
case Result.Failure(let error): throw error
}
}
// Construct a .Success if the expression returns a value or a .Failure if it throws
init(@noescape _ throwingExpr: Void throws -> T) {
do {
let value = try throwingExpr()
self = Result.Success(value)
} catch {
self = Result.Failure(error)
}
}
}
And now we can easily convert our throwing initializer to a closure returning a Result
:
func buildUser(userDict: NSDictionary) -> Result<User> {
// Here we build a `Result` by calling the `init` using a throwing trailing closure
return Result { try User(dictionary: userDict) }
}
And, if we wrap NSURLSession
into a function asynchronously returning a Result
, we can balance between the two worlds however we like. For example:
func fetch(url: NSURL, completion: Result<NSData> -> Void) {
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
completion(Result {
if let error = error { throw error }
guard let data = data else { throw UserError.NoData }
return data
})
}.resume()
}
This also calls the completion
block by passing a Result
object built from a throwing closure4.
Now we can chain it all using flatMap
, and/or go back in the do…catch
world, depending on our needs:
fetch(someURL) { (resultData: Result<NSData>) in
let resultUser = resultData
.flatMap(toJSON)
.flatMap(extractUserDict)
.flatMap(buildUser)
// Then if we want to go back in the do/try/catch world for the rest of the code:
do {
let user = try resultUser.resolve()
updateUI(user: user)
} catch {
print("Error: \(error)")
}
}
I Promise, this is the Future
Result
is cool, but given they are mainly useful in asynchronous functions (since we already have throw
for synchronous function), why not make it so it also manage the asynchronicity?
Well actually, there is a type for that™. It’s called Promise
(and sometimes Future
, the two terms are very alike).
Promise
is a type which kinda brings both the Result
type (which can succeed or fail) and the asynchronicity together. A Promise<T>
can either fulfill with a successful value of type T
when that value becomes available later (hence the async aspect of it), or be rejected if an error occurred.
A Promise
is also a monad. But usually instead of calling the monadic functions by the usual name map
and flatMap
, both are called then
by convention:
class Promise<T> {
// the monadic equivalent of "map", which is usually called "then" in the Promise type
func then(f: T->U) -> Promise<U>
// the monadic equivalent of "flatMap", which is usually called "then" too
func then(f: T->Promise<U>) -> Promise<U>
}
And errors are unwrapped using .error
or .recover
.
At call site, it can mainly be used the same way you’d use a Result
. They are both monads after all:
fetch(someURL) // returns a `Promise<NSData>`
.then(toJSON) // assuming toJSON is now a `NSData -> Promise<NSDictionary>`
.then(extractUserDict) // assuming extractUserDict is now a `NSDictionary -> Promise<NSDictionary>`
.then(buildUser) // assuming buildUser is now a `NSDictionary -> Promise<User>`
.then {
updateUI(user: user)
}
.error { err in
print("Error: \(err)")
}
See how smooth and readable this looks? It’s all about a stream of small processing steps chained together nicely. And it does all the heavy lifting of handling both the asynchronicity and the errors for you. If an error happens during the process, e.g. in extractUserDict
, it jumps directly into the error
block. Like with a do…catch
. Or like with Result
.
The implementation of fetch
to use a Promise
— instead of a completion block and a Result
— would probably look something like this:
func fetch(url: NSURL) -> Promise<NSData> {
// PromiseKit has a convenience `init` which turns a (T?, NSError?) closure into a `Promise`
return Promise { resolve in
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, _, error) -> Void in
resolve(data, error)
}).resume()
}
}
This fetch
method will return immediately, so there is no completionBlock
necessary. But it will return a Promise
object, which will only execute the closure — given via the then
— when the promise is fulfilled and the data (asynchronously) arrives later.
Observe and be Reactive
Promises
are cool, but there is another concept which allows for a stream of small processing steps, handling asynchronicity, as well as handling and propagating errors whenever and wherever they occur during that stream.
This other concept is called Reactive Programming.
Some of you might already know ReactiveCocoa
(RAC in short), or RxSwift
.
Even if it’s sharing some concepts with Promises
(asynchronicity, error propagation, …), that’s the next level beyond Futures
& Promises
: Rx
allows multiple values to be emitted during time (instead of just one return value), and is a lot richer with tons of other features.
But that’s a whole new subject, which will be an exploration for another day!
-
This is only one possible implementation for
Result
. E.g. other implementations might allow to also specify a more specific error type. ↩ -
Here I use a little trick by calling
return completion(…)
in this code instead of callingcompletion(…)
thenreturn
to exit the function scope. This works becausecompletion
returns aVoid
andfetchUser
returns aVoid
too (returns nothing), and becausereturn Void
is equivalent to justreturn
. It’s a matter of taste, but I think it’s nice to be able to write this as a one-liner. ↩ -
In this code, the
@noescape
keyword means that thethrowingExpr
closure is guaranteed to be used directly in the scope of theinit
function before theinit
function returns — in contrast of being stored in a property and used later. This allows the compiler not to force you in usingself.
or[weak self]
at the call site when passing the closure, and be ensured retain cycles are avoided. ↩ -
Take a pause for a second there. See how this code looks really like the one we wrote using
UserBuilder
at the beginning of that article? Feels like we were on the right path 😉 ↩