Oriol
Oriol

Reputation: 635

How are errors handled when combining publishers?

I have started using Combine quite recently and I am trying to create a repository that returns the combination of several data sources. To do that, each data source loads its own data in a quite simple load method:

func loadData() -> AnyPublisher<DataObject, Error>

To combine the data sources I thought of using combineLatest, since it'll wait for the data sources to finish loading and then it'll publish either a combined set with the data or an error indicating that it failed:

func loadData() -> AnyPublisher<[DataObject], Error> {
   return dataSource1.loadData()
             .combineLatest(dataSource2.loadData())
             .map { $0.0 + $0.1 }
             .eraseToAnyPublisher()
}

Overall the behavior of it seems alright, I can call repository.loadData() and I'll get an array that includes the data for both items. However, that's not the case if any of the data sources fail. In that case, the load method will return an error regardless of whether the other data source succeeded.

Is there a standard or recommended way to collect all the errors when combining publishers? In my use context I'd like to be able to discard the error only if both publishers failed, but go through and succeed if only one of them does.

Upvotes: 1

Views: 2086

Answers (1)

rob mayoff
rob mayoff

Reputation: 385500

I think you're saying you want this:

  1. If dataSource1 fails and dataSource2 produces output, discard the failure from dataSource1 and pass along just the output from dataSource2.

  2. If dataSource1 produces output and dataSource2 fails, pass along just the output from dataSource1 and discard the failure from dataSource2.

  3. If both dataSource1 and dataSource2 produce output, pass along the combined outputs.

  4. If both dataSource1 and dataSource2 fail, pass along one of the errors.

I assume each data source produces at most one output. Here's a test setup:

typealias DataObject = String

struct DataSource {
    var result: Result<DataObject, Error>

    func loadData() -> AnyPublisher<DataObject, Error> {
        return result.publisher.eraseToAnyPublisher()
    }
}

We do want to use combineLatest, but we can't let either input to combineLatest fail, because that will make combineLatest fail. We only want combineLatest to fail if both data sources fail. So we need a way to pass a error into combineLatest as an output of one of its input publishers, instead of as a failure of one of its input publishers.

We do that by transforming each input publisher to have an Output of Result<DataObject, Error> and a Failure of Never.

func combine(
    _ source1: DataSource,
    _ source2: DataSource
) -> AnyPublisher<[DataObject], Error> {
    let ds1 = source1.loadData()
        .map { Result.success($0) }
        .catch { Just(Result.failure($0)) }

    let ds2 = source2.loadData()
        .map { Result.success($0) }
        .catch { Just(Result.failure($0)) }

    let combo = ds1.combineLatest(ds2)
        .tryMap { r1, r2 -> [DataObject] in
            switch (r1, r2) {
            case (.success(let s1), .success(let s2)): return [s1, s2]
            case (.success(let s1), .failure(_)): return [s1]
            case (.failure(_), .success(let s2)): return [s2]
            case (.failure(let f1), .failure(_)): throw f1
            }
        }

    return combo.eraseToAnyPublisher()
}

Let's test it:

struct MockError: Error { }

combine(.init(result: .success("hello")), .init(result: .success("world")))
    .sink(
        receiveCompletion: { print($0) },
        receiveValue: { print($0) })
// Output:
// ["hello", "world"]
// finished

combine(.init(result: .success("hello")), .init(result: .failure(MockError())))
    .sink(
        receiveCompletion: { print($0) },
        receiveValue: { print($0) })
// Output:
// ["hello"]
// finished

combine(.init(result: .failure(MockError())), .init(result: .success("world")))
    .sink(
        receiveCompletion: { print($0) },
        receiveValue: { print($0) })
// Output:
// ["world"]
// finished

combine(.init(result: .failure(MockError())), .init(result: .failure(MockError())))
    .sink(
        receiveCompletion: { print($0) },
        receiveValue: { print($0) })
// Output:
// failure(__lldb_expr_28.MockError())

Upvotes: 6

Related Questions