Every Combine publisher defines two associated types, the Output type defines the type of elements the publisher can emit and the Failure type defines the type of errors the publisher can emit. The fact that a publisher is required to define an Output type and a Failure type is convenient, but it can sometimes be inconvenient. What do I mean by that? Take a look at the following example from Building a Modern Networking Layer in Swift.

import Combine
import Foundation

struct APIPreviewClient: APIService {

    // MARK: - Methods

    func episodes() -> AnyPublisher<[Episode], APIError> {
        guard
            let url = Bundle.main.url(forResource: "episodes", withExtension: "json"),
            let data = try? Data(contentsOf: url),
            let episodes = try? JSONDecoder().decode([Episode].self, from: data)
        else {
            fatalError("Unable to Load Episodes")
        }

        return Just(episodes)
            .eraseToAnyPublisher()
    }

}

The episodes() method returns a publisher that has an Output type of [Episode] and a Failure type of APIError. In the body of the episodes() method, the APIPreviewClient struct loads data from a JSON file and creates a publisher using the Just struct. A Just instance is a publisher that emits a single element. We wrap the publisher in a type eraser by applying the eraseToAnyPublisher operator. This seems fine, but the compiler throws an error.

Mismatching Failure Types

Because a Just instance emits a single element, it is guaranteed to not complete with an error. This means that its Failure type is Never. The problem is that the Failure type doesn't match that of the publisher the episodes() method returns, that is, APIError.

Applying the MapError Operator

Let's take a look at a few options to resolve the issue. Can we use the mapError operator to change the Failure type of the publisher to APIError? The compiler complains that Never can't be cast to APIError and that makes sense.

func episodes() -> AnyPublisher<[Episode], APIError> {
    guard
        let url = Bundle.main.url(forResource: "episodes", withExtension: "json"),
        let data = try? Data(contentsOf: url),
        let episodes = try? JSONDecoder().decode([Episode].self, from: data)
    else {
        fatalError("Unable to Load Episodes")
    }

    return Just(episodes)
        .mapError { error in
            error as APIError
        }
        .eraseToAnyPublisher()
}

Mismatching Failure Types

Applying the SetFailureType Operator

Another option is applying the setFailureType operator. As the name suggests, the setFailureType operator replaces the Failure type of the upstream publisher with a type you define. In this example, we use the setFailureType operator to replace the Failure type of the Just instance, Never, with APIError.

func episodes() -> AnyPublisher<[Episode], APIError> {
    guard
        let url = Bundle.main.url(forResource: "episodes", withExtension: "json"),
        let data = try? Data(contentsOf: url),
        let episodes = try? JSONDecoder().decode([Episode].self, from: data)
    else {
        fatalError("Unable to Load Episodes")
    }

    return Just(episodes)
        .setFailureType(to: APIError.self)
        .eraseToAnyPublisher()
}

The only requirement is that the type you pass to the setFailureType(to:) method conforms to the Error protocol. The setFailureType operator should only be applied to a publisher that cannot fail, that is, a publisher with a Failure type of Never. If the upstream publisher can fail, then you need to map the errors of the upstream publisher using the mapError operator.

One of the key differences between the Combine framework and the RxSwift library is that a publisher (Combine) is required to explicitly define its Failure type. The Failure type of an observable (RxSwift) is always Error. By explicitly defining the Failure type of a publisher, error handling becomes easier and more transparent. The downside is that it can sometimes lead to issues you need to address as illustrated in the above example. That downside doesn't outweigh the benefits, though.

What's Next?

The Combine framework defines a number of operators to handle errors, such as tryMap, mapError, and tryCatch. The setFailureType operator is another operator that facilitates error handling.