Catch vs replaceError in Combine

Published by donnywals on

There are several ways to handle errors in Combine. Most commonly you will either use catch or replaceError if you want to implement a mechanism that allows you to recover from an error. For example, catch is useful if you want to retry a network operation with a delay.

The catch and replaceError operators look very similar at first glance. They are both executed when an error occurs in your pipeline, and they allow you to recover from an error. However, their purposes are very different.

When to use catch

The catch operator is used if you want to inspect the error that was emitted by an upstream publisher, and replace the upstream publisher with a new publisher. For example:

let practicalCombine = URL(string: "")!
let donnywals = URL(string: "")!

var cancellables = Set<AnyCancellable>()

URLSession.shared.dataTaskPublisher(for: practicalCombine)
  .catch({ urlError in
    return URLSession.shared.dataTaskPublisher(for: donnywals)
  .sink(receiveCompletion: { completion in
    // handle completion
  }, receiveValue: { value in
    // handle response
  .store(in: &cancellables)

In this example I replace any errors emitted by my initial data task publisher with a new data task publisher. Depending on your needs and the emitted error, you can return any kind of publisher you want from your catch. The only thing you need to keep in mind is that the publisher you create must have the same Output and Failure as the publisher that the catch is applied to. So in this case it needs to be a publisher that matches a data task publisher's Output and Failure.

Note that you cannot throw errors in catch. You must always return a valid publisher. If you only want to create a new publisher for a specific error, and otherwise forward the thrown error, you can use tryCatch which allows you to throw errors.

When to use replaceError

The replaceError operator is slightly simpler than the catch operator. With replaceError you can provide a default value that's used to replace any thrown error from upstream publishers. Note that this operator changes your Failure type to Never because with this operator in place, it will become impossible for your pipeline to fail. This is different from catch because the publisher you create in the catch operator might still fail.

Let's look at an example of replaceError:

enum MyError: Error {
  case failed

var cancellables = Set<AnyCancellable>()
var subject = PassthroughSubject<Int, Error>()

  .replaceError(with: 42)
  .sink(receiveCompletion: { completion in
  }, receiveValue: { int in
  .store(in: &cancellables)

subject.send(completion: .failure(MyError.failed))

If you execute this code in a Playground you'll find that the console will contain the following output:


The first two values are sent explicitly by calling send. The third value is the result of replacing the error I sent as the last value. Note that the publisher completes successfully immediately after. The upstream publisher completes as soon as it emits an error, it's one of the rules of Combine that publishers can only complete once, and they do so through an error event or a completion event.

Note that in catch, the publisher that emitted the error that triggered the catch completes when it emits an error. The publisher you return from catch does not have to complete immediately and it replaces the failed publisher completely. So your sink could receive several values after the source publisher failed because the replacement publisher is still active.

Receive weekly updates about my posts