Bakken & BaeckTech

The Great Callback Hell Bake-Off | Ellen Shapiro

Working with asynchronous code on iOS when using closures to deal with completion blocks can lead to a tremendous amount of nested callbacks. This is a lovely style of code known as Callback Hell.

NOTE: Code which has been simplified for this blog post can be found at https://github.com/designatednerd/OperationComparison. If you'd rather watch a video of a talk based on this article, it's at Swift.Amsterdam.

For example, let's say you have several asynchronous operations: One which fetches a user, one which fetches an image from a given URL, and one which resizes a passed-in image. The basic method signatures would look something like this:

enum Result<T> { case success(_ item: T) case error(_ error: Error) } func fetchUser(completion: @escaping (Result<User>) -> Void) { ... } func fetchImage(for user: User, completion: @escaping (Result<UIImage>) -> Void) { ... } func resizeImage(_ image: UIImage, to size: CGSize, completion: @escaping (Result<UIImage>) -> Void) { ... }

If you just want to call a single one of these methods, the code isn't too bad:

fetchUser { [weak self] result in switch result { case .success(let user): self?.usernameTextField = user.username case .error(let error): debugPrint("Error fetching user: \(error)") } }

You have a fairly clear idea of what you're requesting, and what you're getting back.

The problem comes when you want to chain several of these asynchronous methods together. Here, just using callbacks, is the code to fetch a user, get their avatar, then resize their avatar:

fetchUser { userResult in switch userResult { case .success(let user): fetchImage(from: user) { [weak self] imageResult in switch imageResult { case .success(let image): guard let self = self else { return } resizeImage(image, to: self.imageView.frame.size) { resizedImageResult in switch resizedImageResult { case .success(let resizedImage): self?.imageView.image = resizedImage case .error(let error): debugPrint("Error resizing image: \(error)") case .error(let error): debugPrint("Error fetching image: \(error)") } } case .error(let error): debugPrint("Error fetching user: \(error)") } } }

This deep nesting of callbacks is what's known as Callback Hell. It's awful to read and can be an enormous pain to debug, so iOS developers are always looking for ways to be able to chain asynchronous operations more clearly.

There are a few solutions possible to this problem, all of which have their trade-offs. There are three I'll cover here, each involving a larger investment of time to learn something you haven't work with before, but each giving you more power.

Important Caveat!

Most of this article was written before Apple's Combine reactive framework was announced at WWDC 2019, so it does not cover that framework. Looking forward to messing around with it over the summer though!

Solution #1: Custom
infix
Operators

In Swift, custom operators are operators which are defined by the developer. An

infix
operator is an operator which goes beween two items. For example in the code
let amount = 2 + 3
,
+
is an
infix
operator since it is combining the two things it is in the middle of in order to produce a result.

Declaring something an

infix
operator means allowing you to combine the left and right sides for whatever overrides of the operator you give it. Swift limits the available characters for custom operators, but there's still a reasonable amount of flexibility.

Vincente Pradilles gave a great lightning talk at NSSpain where he used his functional programming skillz to come up with a really interesting solution to this.

In Swift, functions are first-class types. This means can be both parameters and return values from functions. Normally, this is used as you saw in the callback hell examle - to allow passing a completion type.

However, you can also define a particular operator, and pass a function that takes a completion closure to another function which takes the result of the first closure and a second completion closure - returing a third closure function:

infix operator -->: MultiplicationPrecedence func --><T,U>(_ firstFunction: @escaping (@escaping (Result<T>) -> Void) -> Void, _ secondFunction: @escaping (T, @escaping (Result<U>) -> Void) -> Void) -> (@escaping (Result<U>) -> Void) -> Void { return { completion in firstFunction { result in switch result { case .success(let item): secondFunction(item) { result2 in completion(result2) } case .error(let error): completion(.error(error)) } } } }

At first glance, this looks like a salad of generics and functions. What on earth is going on here? First, let's look at the function signature, which is by far the most complicated part.

Overriding the

-->
function tells the compiler, "Here is an implementation of this operator when the left hand side is the first function, the right hand side is the second function, and the desired return value is a third function."

Giving this override generic types of

T
and
U
allows you to make this flexible enough to handle having a second function which may or may not return a different type.

The

firstFunction
parameter takes a function which eventually returns
Void
. That function should have one parameter, which is a
(Result<T>) -> Void
completion block. Remember:
func fetchUser(completion: (Result<User>) -> Void)
? That's what will kick off this chain.

The

secondFunction
parameter takes another function which eventually returns
Void
. That function should have two parameters - one of which is the
T
type of the result of the first function, the other of which is a completion block for the second function, which is looking for type
U
.

The

fetchImage
function takes a
User
as the first parameter and a
(Result<UIImage) -> Void
completion block as the second closure -so we're off to a good start!

Now, we can create a chained equation:

let chain = fetchUser -> fetchImage

The

chain
constant will have the type
(@escaping (Result<UIImage>) -> Void)) -> Void
- This is similar to declaring a function like this:
func chain(completion: @escaping(Result<UIImage>) -> Void))
, but it assigns the function to the
chain
constant instead of creating a reusable function.

Now, you can call

chain
as if it were the function:

chain({ [weak self] result in switch result { case .success(let userImage): // TODO: Get resized image? case .error(let error): debugPrint("Error occurred: \(error)") } })

This is a great place to start. One key problem with this method though: If you want to chain multiple functions, they always have to have a very similar signature. This doesn't help us with resizing the image, because that method takes three parameters: The image to resize, the size to make it, and the completion closure.

In order to make that work with chaining, you have to shoehorn it into another method with the same signature, presumably in the view controller where you wish to display it and where you have access to the size of the view where it's being displayed:

private func resizeImageForImageView(_ image: UIImage, completion: @escaping (Result<UIImage>) -> Void) { resizeImage(image, to: self.imageView.frame.size, completion: completion) }

Once that's done, you can do this:

let chain = fetchUser --> fetchImage --> self.resizeImageToFitImageView

The

(@escaping (Result<UIImage>) -> Void)) -> Void
that was returned from chaining the first two functions is now passed in as the left hand side of the
-->
operator to the third function, and another
(@escaping (Result<UIImage>) -> Void)) -> Void
functioned that encompasses what has happened with all three functions will be assigned to
chain
.

You can still call

chain
as if it were a function, but now it will have the result of all three operations in one place:

chain({ [weak self] result in switch result { case .success(let resizedImage): self?.imageView.image = resizedImage case .error(let error): debugPrint("An error occurred: \(error) } })

You can lose some context around exactly where an error occurred with this method, but if you don't really care in the end, this is a really, really helpful model.

Custom
infix
TL;DR:

Advantages

  • No need for a third party framework

  • You get to feel like a magician with functional programming

  • If it doesn't do what you need, just override the operator function again.

Disadvantages

  • Method signatures can get really ugly and hard to parse

  • Locks you in to a particular style of completion closure unless you do a ton of overrides

  • No way of accounting for threading in this model

  • No way to cancel chains once they're in-flight

  • Not necessarily something other developers will have worked with before.

Solution #2: PromiseKit

PromiseKit is one of several Swift implementations of the Future/Promise pattern, a concept which allows you to declare what will happen when a specified asynchronous operation completes in a way that's more chainable and easier to read.

A

Promise
essentially tells you that "at some point, I am expecting to get an object of this type - and I will let you know when that happens so you can proceed." If errors are received, they're piped through a separate system and are handled separately.

With

PromiseKit
, instead of taking a completion closure of
(Result<T>) -> Void
as a parameter, a function should return a
Promise<T>
.

It's reasonably easy to wrap an existing which takes a completion closure with a promise. For example, fetching the user returns something like this:

func fetchUser() -> Promise<User> { return Promise { seal in fetchUser { // <-- existing method switch result { case .success(let user): seal.fulfill(user) case .error(let error): seal.reject(error) } } } }

You create the promise, which will be kicked off eventually by

PromiseKit
. The
seal
will tell
PromiseKit
's chaining system when the operation is complete, either
fulfill
ing the promise with the object you promised would be there, or
reject
ing the promise with an error.

If you wrap all three methods in this example with

Promise
s, their function signatures would be:

func fetchUser() -> Promise<User> { ... } func fetchImage(for user: User) -> Promise<UIImage> { ... } func resizeImage(_ image: UIImage, to size: CGSize) -> Promise<UIImage> { ... }

Already, this is a bit easier to read and understand, because we've abstracted away the

@escaping
completion closures.

Next, you can use a bunch of built-in methods in

PromiseKit
to set up a chain of requests:

fetchUser() .then { user in fetchImage(for: user) } .then { [weak self] image -> Promise<UIImage> in guard let self = self else { throw GenericException.selfWasDeallocated } return resizeImage(image, to: self.imageView.frame.size) } .done { [weak self] resizedImage in self?.imageView.image = resizedImage } .catch { error in debugPrint("Error occurred: \(error)") }

Note that

then
by default expects a
Promise
to be returned - so if you're only adding one line, and that line is a function which returns a
Promise
, then it will automatically infer that as the return type.

If you have to do other work (like validating that

self
hasn't been deallocated, as in the 2nd
then
above), then you have to give the compiler a few more hints: You need to explicitly declare the return type, and explicitly return the promise from
resizeImage...
.

This code reads much more synchronously, even though it's being executed asynchronously. Each

then
or
done
block doesn't execute until the
Promise
it's chained to fulfills. If a
Promise
is rejected anywhere in the chain, it's piped to the
catch
block and the other
then
blocks and the
done
block don't execute.

You can see a clear path from one step in the chain to another. The

done
block returns a
Promise<Void>
so if you want to wrap the whole chain in a function, you can return there. This is really useful for building small chains of promises into increasingly large chains that can power your whole app.

Alternately, you can add a

catch
block to the end of your chain to handle any error that happens locally. You can't chain anything on to a
catch
block, so it's always going to be the last thing in a chain.

By default,

then
and
done
functions will fire on the main thread. However, those methods (and many others within PromiseKit) also accept a
DispatchQueue
as an argument for where to perform the work within the promise.

This gives you fine-grained control over threading in a way that's just not possible with the custom operator method. You can choose from the call site whether something is doing a big enough operation that it needs to be fired off from a background thread, rather than having to do that work within the method as you would with custom operators.

PromiseKit TL;DR:

Advantages

  • Way more readable than traditional callback hell both at the call site and at the declaration of the function.

  • Has built-in methods of firing requests off on a specified thread (though by default the result is always on the main thread).

  • Automatic piping of errors and tons of methods around recovering from/reacting to errors.

Disadvantages

  • Third-party framework, which means while you're theoretically not responsible for maintenance, you have way more you have to try to understand when something breaks.

  • Designed primarily for handling sequential async events, so if you need to handle two concurrent async events it's probably not a big enough hammer.

  • Cancellation is not really possible in the current version (although it is in progress!)

Solution #3: RxSwift/RxCocoa

RxSwift is a Swift implementation of the

ReactiveX
style of programming. The idea is that almost everything in your application is a reaction to something else which has happened, and that you should chain reactions to actions rather than needing to explicitly tell the computer "When x happens, do y."

RxSwift is particularly powerful because you can combine all of its observable methods and power your UI directly with them.

The actual call to set things up to get the image is not that different from what it looks like in PromiseKit:

let image = FakeAPI.rxFetchUser() .flatMap { user in RealAPI.rxFetchImage(for: user) } .flatMap { image in ImageResizer.rxResizeImage(image, to: self.imageView.frame.size) }

image
here is an
Observable
which you can subscribe to and get information about what happened along the chain. In this case, it's a particular type of
Observable
called a
Single
, which basically means that it will fire once instead of repeatedly.

When you subscribe to a

Single
, you are able to take actions based on whether that
Single
fails or succeeds:

image .subscribe( onSuccess: { [weak self] resizedImage in guard let self = self else { return } self.imageView.image = resizedImage self.operationLabel.text = "Complete (RxSwift) in \(self.formattedSecondsSince(start))!" }, onError: { [weak self] error in self?.operationLabel.text = "Error occurred (RxSwift): \(error)" } ) .disposed(by: bag)

With a more complex operation, you're able to continue listening to a stream of events - and react to each one in turn, or handle any error which occurs.

You're also able to combine observables in a way that allows you to use a related library called RxCococa to bind these actions to your UI. RxCocoa provides the concept of a

Driver
, which can interact with your UI automatically.

For instance, if you want to show a progress indicator while the

image
operation is running, you can create a secondary observable that tracks whether the first observable is running, then convert that into a Driver:

let running = Observable .from([ image.map { _ in false }.asObservable() ]) .merge() .startWith(true) .asDriver(onErrorJustReturn: false)

Note that this

Driver
starts with a given value since the operation would already be running. It also doesn't actually do anything but return
false
when there's an error since the error would be handled elsewhere, and the main thing you want to know - is the operation running or not? - can now be answered.

This bit of code also points up one of Rx's disadvantages: Its verboseness and density. Why is there a

merge
there? I have to be honest, I don't completely understand. The learning curve on making things work can be pretty brutal.

But the reward comes when you're able to easily set up your loading indicator to automatically show and hide based on whether your request is running or not:

running .drive(activityIndicator.rx.isAnimating) .disposed(by: bag)

Once the

running
driver is told to
drive
the
rx.isAnimating
property of your loading indicator, it will automatically handle flipping that on and off. And if you have something potentially combining several operations that needs to drive your UI, this is a dead-simple way of making sure all operations have completed before hiding your loading indicator.

Rx also makes heavy use of

Scheduler
objects to handle threading issues for you - you can easily specify what
Scheduler
you want a particular operation to run on if you'd like, similar to PromiseKit. Some Observable types will come with a default to a particular
Scheduler
- for instance, since
Driver
s are used to drive UI, they're always on the main thread's `Scheduler.

RxSwift/RxCocoa TL;DR:

Advantages

  • Smooth handling of both asynchronous and

  • Ability to easily bind properties to changing values.

  • Ability to compose

Disadvantages

  • Same third-party framework disadvantages as PromiseKit, only more so because it's a larger, more complex project.

  • Very steep learning curve

  • Having to learn schedulers vs. threads vs. dispatch queues to deal with threading

  • This is a pretty significant hammer, and a goodly percentage of the time, your nail isn't actually that big.

Conclusion: What would I use?

I really love the idea of the custom operators, but it has some significant limitations around cancellation and threading. In the end, my personal opinion is that this is a problem best solved by the community, because it is such a monstrously complex problem. (I'd really be interested in seeing community-based custom operator solutions, though!) That leaves me with RxSwift and PromiseKit.

RxSwift is extremely powerful, but in my experience I've found it to be a hammer that's way too big for most of the nails it's applied to. It makes some complex things very simple, but it makes some simple things unreasonably complex. If I've got something super-complicated going on, I will definitely use it. But personally, I usually find it to be total overkill.

I started using PromiseKit on a side project that had a severe problem with callback hell, and it's been a huge help. I've been able to have a much clearer chain of what's happening, facilitating debugging.

I've also found that when working with PromiseKit, I have way fewer modifications that I need to make to existing code to take advantage of what PromiseKit can do, and for me, I think that's it's greatest advantage.