This is Understanding Combine, written by Matt Neuburg. Corrections and suggestions are greatly appreciated (you can comment here). So are donations; please consider keeping me going by funding this work at http://www.paypal.me/mattneub. Or buy my books: the current (and final) editions are iOS 15 Programming Fundamentals with Swift and Programming iOS 14. Thank you!

Retry

.retry (Publishers.Retry) takes an Int parameter, which it stores. If a failure comes down the pipeline from upstream, this operator does not pass the failure on downstream; instead, it decrements the Int, and then unsubscribes itself and subscribes itself to the upstream object, again. This causes the upstream publisher to start over publishing again. This can go on for as long as the original Int permits. If the Int reaches zero and a failure comes down the pipeline from upstream, now this operator sends the failure on downstream.

The classic example of using .retry is with a data task publisher:

URLSession.shared.dataTaskPublisher(for: url)
    .retry(3)

Unfortunately, the .retry operator gives you no way to specify a delay before retrying, so if you think the problem might clear up if you wait a while, you have to insert a .delay operator:

let pub = URLSession.shared.dataTaskPublisher(for: url)
    .delay(for: 3, scheduler: DispatchQueue.main)
    .retry(3)

But that’s not an ideal solution, because the .delay runs regardless of whether there’s an error or not. What we’d like to do is insert the .delay operator only if there was a failure.

That problem can be solved through the use of .catch. The .catch operator’s function runs only if there is an error from upstream. When we return a publisher from that function, it replaces the upstream publisher and becomes the new publisher. But what publisher should we return?

let pub = URLSession.shared.dataTaskPublisher(for: url)
    .catch {
        // what goes here?
        .delay(for: 3, scheduler: DispatchQueue.main)
    }
    .retry(3)

Well, if the .catch function runs, we want what passes down the pipeline to the .retry operator to be a failure; that, after all, is the whole point of using .retry in the first place. The simplest possible publisher expressing failure is the Fail value publisher. An error has arrived into the .catch block; we turn right around and pass the same error out of the .catch block, wrapped in a Fail, and delayed by a .delay:

let pub = URLSession.shared.dataTaskPublisher(for: url)
    .catch {
        Fail(error: $0)
            .delay(for: 3, scheduler: DispatchQueue.main)
    }
    .retry(3)

That works correctly. If the initial data task publisher fails with an error, the catch function runs and returns a new publisher, a Fail that publishes the very same error, with a .delay attached to it, to be handled by the .retry. If the data task publisher succeeds, the .catch is skipped, the .retry is skipped, and the value from the data task publisher passes on down the pipeline.

I owe this approach entirely to reader @moreindirection.