Retrying async functions in Swift via high-order functions

TL;DR; Solution can be also found here.

Supposing we’re writing a client-server communication layer, and we want to add support for retrying failed calls for a number of times before giving up and reporting error. A classical example would be updating a user’s profile when pressing a Save button from the UI.

A typical function might look like this:

func updateProfile(firstName: String, lastName: String, success: Any -> Void, failure: NSError -> Void) {
    let urlRequest = NSURLRequest()
    // ...
    NSURLSession.sharedSession().dataTaskWithRequest(urlRequest,
        completionHandler: { data, response, error in
            // if we have no error and have received data, we're OK
            guard error == nil, let data = data else {
                failure(error ?? NSError(domain: "MyDomain", code: 1, userInfo:  nil))
                return
            }
            success(data)
    }).resume()
}

How can we make this retry-able any number of times we need?

Firstly, let’s add a type alias corresponding to a no-parameter async function that we can
pass around:

typealias Async = (success: Void -> Void, failure: NSError -> Void) -> Void

Next, we need to modify the `updateProfile` function to return an `Async` value, and the `retry` function to make use of that value. This change doesn’t require much effort, it’s just a matter of removing the callbacks from the parameters list and wrapping the function body into a closure:

func updateProfile(firstName: String, lastName: String) -> Async {
    return {success, failure in
        let urlRequest = NSURLRequest()
        // ...
        NSURLSession.sharedSession().dataTaskWithRequest(urlRequest,
            completionHandler: { data, response, error in
                // if we have no error and have received data, we're OK
               guard error == nil, let data = data else {
                   failure(error ?? NSError(domain: "MyDomain", code: 1, userInfo: nil))
                   return
               }
               success(data)
        }).resume()
    }
}

We also need the actual retry function, which could look like this:

func retry(numberOfTimes: Int, task: () -> Async, success: Void -> Void, failure: NSError -> Void) {
    task()(success: success, failure: { error in
        if numberOfTimes > 1 {
            retry(numberOfTimes - 1, task: task, success: success, failure: failure)
        } else {
            failure(error)
        }
    })
}

One minor inconvenient is that the retry function expects a closure that takes no arguments and returns an Async, while our updateProfile function takes two arguments. This is not however a big problem, as we can wrap the updateProfile call into a closure:

retry(3, task: { updateProfile("firstName", lastName:"lastName") },
    success: { data in
        print("Succeeded")
    },
    failure: { err in
        print("Failed: \(err)")
    }
)

Nice and clean, we can easily configure the number of times to retry, as most of all our existing async functions need very few changes in order to benefit of this retry mechanism.

P.S. If however you don’t want or can’t change existing functions, then you can simply add wrappers for those functions, like in this example for updateProfile:

func updateProfileAsync(firstName: String, lastName: String) -> Async {
    return { success, failure in
       updateProfile(firstName, lastName: lastName, success: success, failure: failure)
    }
}

Join the Conversation

7 Comments

    1. I have a question, shouldn’t the Async function be like this?

      “` typealias Async = (_ success: @escaping (URLResponse?,Data) -> (), _ failure: @escaping (URLResponse?, Error?) -> ()) -> ()

      Cheers,

      1. I understood it now. Because maybe we handle the response and data inside the function already, we don’t need to pass them out.

      2. @allenlinli – the code samples are for Swift 2.x, likely some adaptations will be needed to make them work from Swift 3. I’ll try to update the blog post to work for Swift 3 also. And yes, the URLResponse can be part of the success/failure callbacks, for simplicity I didn’t pass them along.

  1. How can we write the Async for a generic method like func func1(var para1, @escaping(Response, Error) -> Void) ?

    1. The Async could look like this: typealias Async = (success: Response -> Void, failure: NSError -> Void) -> Void.
      Since the article was written, Swift got the capability of declaring generic type aliases, so the Async definition could be generic: typealias Async = (success: T -> Void, failure: NSError -> Void) -> Void

Leave a comment

Your email address will not be published. Required fields are marked *