Avoiding Callback Hell in Swift

Avoiding Callback Hell in Swift

Being able to work in projects of the most diverse varieties gave me the chance to be in contact with several types of developers and code bases. Besides their core differences, what stood out to me during this process is that projects with a lower level of maturity will always face similar problems.

Perhaps they choose the wrong architecture, or the lack of unit tests caused a nasty bug to sneak into production, but there's a specific problem that always draws my attention - callback hell. If not treated from the very beginning, these awful pyramids of braces you get when chaining callbacks inside other callbacks or conditions plague code bases with an eternity of impossible code-reviews and distant screams of "what the hell is this method supposed to be doing?".

private func requestNetwork<T: Request>(request: T, completion: (Result<T> -> Void)?) {
  if isUserLogged {
    do {
      let urlRequest = try request.toRequest()
      session.dataTask(with: urlRequest) { (data, response, error) in
        if let httpResponse = response as? HTTPURLResponse {
          if acceptedStatuses?.contains(httpResponse.statusCode) != true {
            if let apiError = errorParser?.possibleError(from: data) {
              completion(.failure(error))
              return
            }
          }
        }
        preprocess(data) { (processedData, error) in
          if let error = error {
            completion(.failure(error))
          }
          if let processedData = processedData {
            do {
              let result = try request.serialize(processedData)
              completion(.success(result))
            } catch {
              completion(.failure(error))
            }
          } else {
              completion(.failure(HTTPError.unknown))
          }
        }
      }
    } catch {
      completion(.failure(error))
    }
  } else {
    completion(.failure(HTTPError.loggedOut))
  }
}

They are difficult to read, nearly impossible to review but unfortunately super easy to write, cementing their place as the bane of junior developers.

Fortunately for us, Swift offers several options to avoid this behavior. With a bit of patience and a proper style guide, you can prevent this sort of mistake from affecting your productivity. I'll use this article to share how I personally avoid them, and hopefully this will help you come up with your own solutions.

Condition hells: Favor using guard instead of if

Pyramids of conditions are very common, and fortunately the easier to deal with. guard is on my top 10 features in Swift for a good reason - although it works basically as an inverted if statement, it gives you a great advantage in terms of code quality. Besides providing a way for you to give an early return to a method, it allows you to put the "good" outcome of a method in the same indentation as the method itself, making your method's intent far easier to be understood by your colleague. The improvement is not difficult to spot in a chain of if statement:

func foo() {
    if a {
        if b {
            if c {
                //Good outcome
            } else {
                //Bad outcome 3
            }
        } else {
            //Bad outcome 2
        }
    } else {
        //Bad outcome 1
    }
}
func foo() {
    guard a else {
        return //Bad outcome 1
    }
    guard b else {
        return //Bad outcome 2
    }
    guard c else {
        return //Bad outcome 3
    }
    //Good outcome
}

If you embrace the mindset of putting the good outcome of your method as close as possible to the method's indentation and the bad outcomes as far as possible from it, you'll find your code to be significantly easier to read as a mere glance at the end of the method will be enough for someone to understand what it's supposed to do. Use guards to isolate things that are not supposed to happen and restrict the usage of ifs to things that aren't necessary for the good outcome to happen, like changing the color of a cell based on a property's value.

func updatePromotions(animated: Bool = true) {
    guard isUserLogged else {
        displayLoginScreen()
        return
    }
    if animated {
        delegate?.didStartLoading()
    }
    //Good outcome: Fetch promotions
}

Closure hells: Abstracting completion handlers

Callback hells caused by asynchronous calls are the trickier to solve as completion handlers can contain pretty much anything, but there are efficient ways to deal with them as well.

Promises

The concept of Promises is my go-to solution for managing anything that's asynchronous. If you have never seen them before, Promises relate to the concept of a type that may or may not resolve a value at a later time:

func getInt() -> Promise<Int>
    return Promise { promise in
       //Do something async
       promise.fulfill(number)
       //Or promise.fail(error)
    }
}
let promise = getInt().then { number in
    print(number * 10) //if it succeeds
}.catch { error in
    print(error) //if it fails
}

The Promise type can receive closures that determine how to proceed depending on the result of resolving the value, represented by then(completion:) and catch(completion:) in this case. If you're wondering why this helps with callback hells, it's because then handlers can optionally receive another promise, creating a limitless straight flow of operations:

func perform<T: Request>(request: T) -> Promise<T.Response>
    return Promise { promise in
       //Do the actual request here, then:
       promise.fulfill(response)
    }
}

perform(requestOne()).then { responseOne in 
    perform(requestTwo(responseOne: responseOne))
}.then { responseTwo in
    perform(requestThree(responseTwo: responseTwo))
}.then { responseThree in
    perform(requestFour(responseThree: responseThree))
}.catch { error in
    print(error)
}.always {
    print("Finished")
}

By making your async operations return Promise types instead of receiving completion handlers, you will be able to chain any amount of them together into a nice straight line of code. They are specially great when your operations depend on things returned by previous operations as more powerful Promise implementations will contain several options for transforming values as well.

I personally use PromiseKit as it contains tons of features, but there are lightweight libraries around the web and you could certainly develop a simple Promise implementation yourself.

You'll see people recommending things like RxSwift for this purpose as well - I would personally not do so because I think that anything that holds your entire project hostage is a death sentence in the long term (as in, every single thing you do has to take RxSwift's architecture in mind in order to work), but that's my personal opinion and you can definitely use it if you know what you're doing.

"I don't want to add more code!": OperationQueue

If Promises aren't your thing because you'd rather solve things the Apple way, you can use Foundation's native solutions for managing sequential operations.

OperationQueue is Apple's abstraction of DispatchQueue that contains additional features to better support synchronizing and canceling operations. If your operations don't rely on data from previous operations, the Operation family of classes will do the trick. For synchronous operations, this is just a matter of queuing your custom operations:

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1

func doLotsOfStuff(completion: (() -> Void)?) {
    let firstOperation: Operation = FirstOperation()
    let secondOperation: Operation = SecondOperation()
    secondOperation.completionBlock = {
      completion?()
    }
    secondOperation.addDependency(firstOperation)
    queue.addOperation(firstOperation)
    queue.addOperation(secondOperation)
}

However, things are trickier for asynchronous operations. To make the queue wait for your operation to truly finish, you'll either have to use thread-blocking mechanisms such as DispatchGroups or create/use a custom AsynchronousOperation type that manages an Operation's states for this purpose.

If you need an operation to pass data to another one, you'll find no clean solution with OperationQueue as there's no guarantee that an operation's completionBlock will be called before the next one starts running. There a few hacks to achieve them through - you can wrap all your needed data in an external reference type that is accessible by all operations:

func doLotsOfStuff(completion: (() -> Void)?) {
    let data = NeededOperationData()
    // data has all properties needed by all operations
    // and each operation fetches and sets the ones
    // needed by the next operation.
    let one = OperationOne(data)
    let two = OperationTwo(data)
    two.addDependency(one)
    let three = OperationThree(data)
    three.addDependency(two)

    queue.addOperation(one)
    queue.addOperation(two)
    queue.addOperation(three)
}

Alternatively, you can store the necessary data in the operation's dependency and access it by subclassing the operation and fetching its dependencies when it gets executed.

class SecondOperation: AsynchronousOperation {
    var data: Data?

    override func main() {
      super.main()
      let firstOperation = dependencies.first as? FirstOperation
      data = firstOperation.result
      //Run the operation
    }
}

I dislike having to deal with optional properties everywhere, so I personally wouldn't use OperationQueue if my operations depended on data fetched by other operations.

"Foundation sucks!": Use high-order functions

If you want to do this without using additional data structures, you can treat callback hell with nothing but pure Swift by applying better coding practices and some principles from functional programming. Because closures are types, they can be passed as arguments to methods - normally as completion handlers. The thing is that Swift methods are just glorified closures, so you can pass an entire method as a closure argument. This exact concept can be used to reduce the amount of nested closures in a method:

let sum = array.reduce(0, +)
//reduce() here is an ((Int, ((Int, Int) -> Int)) -> Int)
//and the + operator is func +(lhs: Int, rhs: Int) -> Int,
//... or ((Int, Int) -> Int), so there's no need to define reduce's closure.

To see how this applies, let's assume that we have a method that downloads a picture from the web, locally applies a Sepia tone filter to it in another thread and then uploads it as the user's profile picture:

func applySepiaFilterAndUpload(picUrl: URL, completion: ((User) -> Void)?) {
    session.perform(downloadPictureRequest(url: picUrl)) { data in
        filtersQueue.async {
            let filteredPicture = applySepiaFilterTo(picData: data)
            session.perform(uploadUserPictureRequest(data: filteredPicture)) { user in
                completion?(user)
            }
        }
    }
}

I've left out any kind of error management to make this article easier to grasp, but as any classic callback hell problem, the first problem here is clear: this method does way too much stuff. Before we start thinking about the closures, let's first apply the single responsibility principle and divide each part of this workflow into separate methods:

func downloadPicture(fromUrl url: URL, completion: ((Data) -> Void)?) {
    session.perform(downloadPictureRequest(url: url)) { data in
        completion?(data)
    }
}

func applySepiaFilter(toPicData data: Data, completion: ((Data) -> Void)?) {
    filtersQueue.async {
        let filteredPicture = applySepiaFilterTo(picData: data)
        completion?(filteredPicture)
    }
}

func uploadUserPicture(data: Data, completion: ((User) -> Void)?) {
    session.perform(uploadUserPictureRequest(data: data)) { user in
        completion?(user)
    }
}

func applySepiaFilterAndUpload(picUrl: URL, completion: ((User) -> Void)?) {
    downloadPicture(fromUrl: picUrl) { data in
        applySepiaFilter(toPicData: data) { filtered in
            uploadUserPicture(data: filtered) { user in
                completion?(user)
            }
        }
    }
}

Although the callback hell still exists here, we at least have something that's readable now.

To reduce the amount of nested closures, analyze how this method works. Can you see the pattern in applySepiaFilterAndUpload()? The key to solving the nesting problem here is how each step works: every method here works in the exact same way. downloadPicture receives an URL and provides a Data completion, applySepiaFilter receives a Data and provides another Data completion, and uploadUserPicture receives a Data and provides a User completion. If you turn these types into generics, you'll end up with:

downloadPicture    = (T, (U -> Void)) -> Void
applySepiaFilter   = (U, (V -> Void)) -> Void
uploadUserPicture  = (V, (W -> Void)) -> Void

Because these async operations have the exact same structure and clearly depend on each other, we can completely remove the necessity of having closures by adapting these methods to recieve the next one as an argument. This would be trivial to do if each method had an explicit return type, but since we're dealing with completion handlers we need to write a little helper to achieve this effect. First, I'll define this shared behaviour as an Operation alias (with optionals so nobody's forced to do anything):

public typealias Operation<T, U> = ((T, ((U) -> Void)?) -> Void)?

With that, we can define a method that "merges" two operations into one as long as they have matching parameters - making (T, (U -> Void)) -> Void + (U, (V -> Void)) -> Void become (T, (V -> Void)) -> Void:

func merge<T, U, V>(_ lhs: Operation<T, U>, to rhs: Operation<U, V>) -> Operation<T, V> {
    return { (input, completion) in
        lhs?(input) { output in
            rhs?(output, completion)
        }
    }
}

This method returns a new closure that performs the first operation method with a given input, uses its output to execute the second one and finally executes a given completion for the second operation's result. If all our methods follow the Operation structure, we can use merge() to progressively merge all steps into a single operation. We can't really escape the nesting in this helper, but this allows us to rewrite our main method without them:

func applySepiaFilterAndUpload(picUrl: URL, completion: ((User) -> Void)?) {
    let job = merge(merge(downloadPicture, to: applySephiaFilter), to: uploadUserPicture)
    job?(picUrl, completion)
}

Because the signature of our operations match merge()'s closure arguments, we can skip having to define closures by passing the methods' signatures as the arguments. In the end, job becomes an unified method that takes an URL, executes all operations in order and then finally the executes the method's completion handler with the result of the last operation. That's just like the first version, but with no nesting at all!

Now, if you're thinking "but that looks terrible!", you're absolutely right. Because we can only merge two operations at a time, we need to call merge() several times which will result in something that's probably harder to read than the original callback hell. There's a way to fix this though - we can define an operator for merge()'s behavior:

infix operator >>->>: LogicalConjunctionPrecedence // Precedence of &&

func >>->><T, U, V>(lhs: Operation<T, U>, rhs: Operation<U, V>) -> Operation<T, V> {
    return merge(lhs, rhs)
}

By using &&'s precedence, operations will be progressively merged all the way from the left. With that in place, we can now rewrite our workflow as a nice straight line of operations.

func applySepiaFilterAndUpload(picUrl: URL, completion: ((User) -> Void)?) {
    let job = downloadPicture >>->> applySepiaFilter >>->> uploadUserPicture
    job?(picUrl, completion)
}

If you're into this sort of stuff, the formal name for this very specific merging operation is the Kleisi composition.

Conclusion: Read articles and books about writing clean code

If you take a deep look at it, you'll notice that the presence of things like callback hell will always boil down to the lack of good coding practices.

Clean code is a big topic, but there are great resources about it around the web. I've personally read and highly recommend Robert C. Martin's Clean Code book as it teaches you how to see your code from the perspective of other developers - a great skill to have when learning how to write better looking code. You should definitely give it a try if you're a professional developer.

Follow me on my Twitter - @rockbruno_, and let me know of any suggestions and corrections you want to share.

References and Good reads

Promises
Kleisi composition
WWDC: Advanced NSOperations
AsynchronousOperation.swift