Why does merging or combining publishers fail to compile with “types Never and E must be equivalent”, and how do setFailureType, mapError and tryMap fix it?
Predict first
Does merging a Never-failing publisher with one that fails with E compile?
import Combine
struct E: Error {}
let a = Just(1)
let b = Fail<Int, E>(error: E())
let both = a.merge(with: b)
- No, compile error
- Yes
Predict first
And after setFailureType, plus tryMap then mapError?
import Combine
struct E: Error {}
let a = Just(1).setFailureType(to: E.self)
let b = Fail<Int, E>(error: E())
let both = a.merge(with: b)
let converted = Just(1).tryMap { $0 }.mapError { _ in E() }
_ = (both, converted)
- Yes, it compiles
- No, compile error
Mental model. Plugs must match the socket. setFailureType is a plug adapter for a connector that can never carry current; mapError is a converter between two real standards.
Every publisher has an Output and a Failure, and operators that combine publishers (merge, combineLatest, zip, flatMap) require the failure types to be identical. Just(1) has Failure == Never, while Fail<Int, E> fails with E, so merging them is a compile error (“instance method ‘merge(with:)’ requires the types ‘Never’ and ‘E’ be equivalent”). The fix is to make the types agree explicitly: setFailureType(to: E.self) upgrades a Never-failing publisher to one that can fail with E (it never actually fails), and mapError converts one error type to another; tryMap and the other try operators make the failure type Error (the existential), which then needs mapError to get back to a specific type.
Design guidance: use one app-level error type per layer (a NetworkError, a RepositoryError) and convert at the boundary with mapError, so downstream code is not littered with Error; keep Never failure for UI-facing publishers (Q311) by handling errors in the inner publisher; and use eraseToAnyPublisher() at API boundaries so the long generic type does not leak. Since Swift 6 typed throws, async code has a similar story: prefer a specific error type when it is meaningful, or any Error when it is not. The compiler messages here are verbose but always point at the mismatch; read them for Failure and fix the type, never cast.
Failure types must be equal for merge.
setFailureType and mapError make the types agree.
Common traps
- Fighting the compiler with
as!or force-casting errors. Convert withmapError. - Using
tryMap“just in case”. It widens the failure toErrorand infects the rest of the pipeline.
They might follow up with
- When can I use
assign(to:on:)? Only whenFailure == Never; so handle errors first. - Why does
catchchange the failure type? The replacement publisher’s failure becomes the new one, socatch { Just(…) }yieldsNever.
Related questions
- 11.10.1 How do you end a pipeline: prefix(untilOutputFrom:), cancel and completion, and why does a sink that captures…
- 11.7.3 Where should a catch go in a search pipeline, and what happens to the stream if you put it outside the…
- 11.5.2 How do failures work in Combine, what does Never mean, and how do catch, replaceError, retry and mapError…
- 11.6.1 When do you use Combine, AsyncSequence or the Observation framework, and when should you not use Combine?
← How do you end a pipeline: prefix(untilOutputFrom:), cancel and…