Teabyte

Mobile app development for iOS and Swift

Fulfill your promises

2019-08-31

At work we are currently working on an app which involves quite some complicated data flows, in which we have to work with backend calls that need to be transferred to bluetooth devices. And those calls are not only coming from backend -> bluetooth device but also from backend -> bluetooth device-> backend. As you can imagine these data flows are not working synchronously but asynchronously.

Anyone who has already worked with multiple asynchronous method calls may know the term "Callback Hell". It describes a pattern in code where you have a lot of nested callbacks from async methods. Let us look at a quick pseudo-code example. It assumes the callback methods return a Result<T, Error> type.

/// Deeply nested async method calls
callBackend(id: 1) { result in
    switch result {
    case let .success(data):
        checkBluetooth { result in
            switch result {
            case let .success(peripheral):
                transferDataToBluetooth(peripheral: peripheral, data: data) { result in
                    switch result {
                    case let .success(bluetoothReturnData):
                        transferDataToBackend(bluetoothReturnData) { result in
                            switch result {
                            case let .success(backendReturnData):
                                //finish
                            case let .failure(error):
                                //handle error
                            }
                        }
                    case let .failure(error):
                        //handle error
                    }
                }
            case let .failure(error):
                //handle error
            }
        }
    case let .failure(error):
        //handle error
    }
}

It can be easily seen that this type of code is:

  • very hard read, even harder for someone who is not familiar with the codebase
  • error prone since we have to handle failures all over the place

One might argue that the underlying idea of having such complicated flows is the first problem we would need to tackle, but sometimes we have to live with certain guidelines by customers. In such cases it's the task of developers transform "not so nice data flows" in easy to understand, easy to read data flows. That's the place where Promises come to our rescue.

Promises

The idea of simple. A promise is container which holds a value and promises that you will get the value at some point in time. This process can either be successful or fail with an associated error. With Promises we have the ability to chain asynchronously sequentially eliminating the need of nested callbacks and catch failed calls in a common place.

There are different libraries which implement the promise pattern in Swift, but in this post I want to highlight PromiseKit. It's an easy drop-in replacement for all callback based APIs making it a breeze to work with promises.

Since code is always better than words, let us convert our callback based example from the beginning to Promises:

import PromiseKit
 
firstly {
    callBackend(id: 1)
}.then { data -> Promise<(Data, CBPeripheral)> in
    return checkBluetooth().map {($0, peripheral)}
}.then { peripheral, data  in
    transferDataToBluetooth(peripheral: peripheral, data: data)
}.then { bluetoothReturnData in
    transferDataToBackend(bluetoothReturnData)
}.done { backendReturnData in
    // finish
}.catch { error in
    // handle error
}

Wow, so much cleaner ! Even for someone who is not familiar with the codebase it is easy to understand what is going on in this flow. Every call is chained with each other and we have a common catch phrase when one of the calls inside the chain fails.

But let use dive a little bit deeper into the code and look at some places where it gets interesting. First, let us have a look at some common methods inside PromiseKit.

Firstly

firstly {
    callBackend(id: 1)
}.then { ...

firstly marks the beginning of a promise chain. It is just syntactic sugar for

callBackend(id: 1).then { ...

then

}.then {...

then is used to chain promises together and is expected to return another Promise<T>. In most of the cases you do not need to declare the return type of the then block. But sometimes the swift compiler fails to infer the return type correctly which makes it necessary to declare the return type explicitly (like we did in line 5).

Pass data between thens

This then block additionally shows a technique to pass data from a previous then further down the chain. We simply map it into a tuple. This is a common pattern where it is necessary to declare the return type explicitly.

}.then { data -> Promise<(CBPeripheral, Data)> in
    return checkBluetooth().map {($0, data)}
}.then { ...

done

}.done { ...
    // finish
}.

done, as the name implies, marks the end of a promise chain and is expected to return a Guarantee<T>, which is nothing more than a Promise<T> that can no longer be chained and does not fail.

catch

}.catch { error in
    // handle error
}

The last keyword I want to talk about is catch. It is a block which catches any error that has occurred in any of the chained promise calls. You can do all of your error handling in one common place and do not have it scattered around in deeply nested callbacks.

More

There are many, many more flavours to PromiseKit than I have covered now. I definitely recommend to check out their awesome documentation.

Transform a callback into a Promise

Now, that he talked a lot about Promises and how to use them in Swift, the question is: "How do I transform my callback based APIs into Promises ?" The answer is quite easy. You can leave your functions untouched and just wrap them into a promise based version and start to use that instead of the callback based one. Let us look at an example:

Imagine having a callback based call that looks like this:

func callBackend(id: Int, _ callback: @escaping (Swift.Result<Data, Error>) -> Void) {
...
}

To wrap this into a promise we firstly declare an extension function on the Resolver type of PromiseKit.

extension Resolver {
 
    func resolve(from result: Swift.Result<T, Error>) {
        switch result {
        case let .success(t):
            self.fulfill(t)
        case let .failure(error):
            self.reject(error)
        }
    }
}

And then make us of it to declare a promise based version of the callBackend function:

func callBackend(id: Int) -> Promise<Data> {
    return Promise { seal in
        self.callBackend(id: id) { result in
            seal.resolve(from: result)
        }
    }
}

With that procedure you can step by step adapt to promise based API calls and do not introduce breaking changes into your codebase.

Conclusion

Promises can extremely simplify multiple sequentially executed asynchronous method calls. They bring clarity and structure into the codebase, making it easy, even for new developers, to grasp what's going on. Furthermore they minimize error prone code and force us to handle errors in a common place - much more organized than with nested callbacks.

Goodbye Callback Hell ! 👋