Skip to content

Instantly share code, notes, and snippets.

@rdv0011
Last active September 5, 2022 09:25
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rdv0011/200f0edf9a2e244daa72e514fc314c56 to your computer and use it in GitHub Desktop.
Save rdv0011/200f0edf9a2e244daa72e514fc314c56 to your computer and use it in GitHub Desktop.
Retry operation in Swift/Combine.

There is a function .retry() in Combine that helps to retry a request when something goes wrong with a long runing task. Although it is not enough just to call retry() to achieve retrying logic. The retry() function does not do another request but it re-subscribes only to a publisher. The below approaches might be used to solve the problem.

Using tryCatch

To make another request the tryCatch() might be used. In the code below if the first call fails there are three attempts to retry (retry(3)) that are made:

import UIKit
import Combine
import PlaygroundSupport

enum CustomNetworkingError: Error {
    case invalidServerResponse
}

let backgroundQueue: DispatchQueue = DispatchQueue(label: "backgroundQueue")
let backendURL = URL(string: "https://google1.com")!

func dataPublisher(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
    Future<(data: Data, response: URLResponse), URLError> { promise in
        print("Make a request")
        backgroundQueue.asyncAfter(deadline: .now() + 1) {

            promise(.failure(URLError(.notConnectedToInternet)))
        }
    }
    .eraseToAnyPublisher()
}

func dataLoader(backendURL: URL) -> AnyPublisher<Data, Error> {
    let request = URLRequest(url: backendURL)
    print("DataLoader")

    return dataPublisher(for: request)
        // We get here when a request fails
        .tryCatch { (error) -> AnyPublisher<(data: Data, response: URLResponse), URLError> in
            print("Try to handle an error")
            guard error.code == .notConnectedToInternet else {

                throw error
            }
            print("Re-try a request")

            return dataPublisher(for: request) // <-- This is a point where another request is made
        }
        .tryMap { data, response -> Data in
            guard let httpResponse = response as? HTTPURLResponse,
                httpResponse.statusCode == 200 else {

                throw CustomNetworkingError.invalidServerResponse
            }
            return data
        }
        .eraseToAnyPublisher()
}

let anyCancellable = dataLoader(backendURL: backendURL)
    .retry(3) // <-- Literal constant regulates how many times we will end up in tryCatch() when a request fails
    .subscribe(on: backgroundQueue)
    .receive(on: RunLoop.main)
    .sink(receiveCompletion: { (result) in
        switch result {
        case .finished: ()
        case .failure(let error):
            guard let error = error as? URLError else {
                return
            }
            print(error)
        }
        PlaygroundPage.current.finishExecution()
    }) { (data) in
        print(data)
    }

PlaygroundPage.current.needsIndefiniteExecution = true

The output of this code follows below:

DataLoader
Make a request
Try to handle an error
Re-try a request
Make a request
Try to handle an error (first attempt to retry)
Re-try a request
Make a request
Try to handle an error (second attempt to retry)
Re-try a request
Make a request
Try to handle an error (third attempt to retry)
Re-try a request
Make a request
URLError(_nsError: Error Domain=NSURLErrorDomain Code=-1009 "(null)")

Using Deferred {}

func dataPublisher(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
    Deferred {
        Future<(data: Data, response: URLResponse), URLError> { promise in
            print("Make a request")
            backgroundQueue.asyncAfter(deadline: .now() + 1) {

                promise(.failure(URLError(.notConnectedToInternet)))
            }
        }
    }
    .eraseToAnyPublisher()
}

In this case there is no need to use tryCatch {}. The downside of this approach is that there is no way to control whether we need to make another try or not. In this case it means three retries will be made regardless of the return value.

DataLoader
Make a request
Make a request
Make a request
Make a request
URLError(_nsError: Error Domain=NSURLErrorDomain Code=-1009 "(null)")
@mesheilah
Copy link

I couldn't understand the downside for the second approach: "... The downside of this approach is that there is no way to control whether we need to make another try or not...."
can you elaborate more ?
I can use any kind of operators with the publisher returned from deferred.

@rdv0011
Copy link
Author

rdv0011 commented Jan 21, 2020

I couldn't understand the downside for the second approach: "... The downside of this approach is that there is no way to control whether we need to make another try or not...."
can you elaborate more ?
I can use any kind of operators with the publisher returned from deferred.

That is true, but you do not control the execution flow in this case, which means three retries will be made regardless of the return value.

@FeedMyTummy
Copy link

three retries will be made regardless of the return value.

Could you give an example of what you mean by this?

@dry1lud
Copy link

dry1lud commented Jun 3, 2020

three retries will be made regardless of the return value.

Could you give an example of what you mean by this?

In the first approach there is a possibility to stop before making a next network call based on some condition. In the code it means the following:

            guard error.code == .notConnectedToInternet else {

                throw error // By throwing an error we stop re-trying
            }
            print("Re-try a request")

            return dataPublisher(for: request) // <-- This is a point where another request is made. Here we continue re-trying.

Please take a look at two points above: "stop re-trying" and "continue re-trying"

There is no such option in the second approach where we specify an amount of retry times only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment