New Combine operators do not need a hand-written Publisher conformance. Compose existing
operators inside a Publisher extension and erase the type. Three operators cover the error
handling most apps repeat.
replaceErrorOrEmpty
replaceEmpty and replaceError almost always travel together; fuse them.
extension Publisher {
func replaceErrorOrEmpty(with output: Output) -> AnyPublisher<Output, Never> {
self
.replaceEmpty(with: output)
.replaceError(with: output)
.eraseToAnyPublisher()
}
}
finishOnFail
Sometimes a failure should just end the stream silently, such as a remote refresh merged with cached data: the cache already covers the failure case.
extension Publisher {
func finishOnFail() -> AnyPublisher<Output, Never> {
self
.catch { _ in Empty() }
.eraseToAnyPublisher()
}
}
Publishers.Merge(
newsService.fetchCachedNews(),
newsService.fetchRemoteNews().finishOnFail()
)
.map(Action.setNews)
catchResult
When the error belongs in app state, wrap value and failure into one Result so the
publisher itself never fails.
extension Publisher {
func catchResult() -> AnyPublisher<Result<Output, Failure>, Never> {
self
.map(Result.success)
.catch { Just(Result.failure($0)) }
.eraseToAnyPublisher()
}
}
In a Redux-style store this is the natural shape for side effects: a reducer's effect must
never fail, so a failure becomes an ordinary action carrying Result.failure.
case .fetchAuth:
return environment.service
.authorize()
.catchResult()
.map(Action.setAuth)
.eraseToAnyPublisher()
case .setAuth(let result):
state.auth = result
return Empty().eraseToAnyPublisher()