What are Publisher, Subscriber, Subscription and Subscriber.Input/Failure, and when does a pipeline actually start?
Predict first
A pipeline is built with a side effect on subscription. What is the side-effect counter before and after the sink?
import Combine
var side = 0
let p = [1, 2, 3].publisher.handleEvents(receiveSubscription: { _ in side += 1 })
print("before", side)
var bag = Set<AnyCancellable>()
p.sink { _ in }.store(in: &bag)
print("after", side)
- before 1, after 1
- before 0, after 1
- before 0, after 0
Predict first
What order of events does a sink see for a two-element array publisher?
import Combine
var ev: [String] = []
var bag = Set<AnyCancellable>()
[1, 2].publisher.sink(receiveCompletion: { _ in ev.append("done") }, receiveValue: { ev.append("v\($0)") }).store(in: &bag)
print(ev)
- ["v1", "v2", "done"]
- ["done", "v1", "v2"]
- []
Predict first
A Future is created but nobody subscribes. Has its closure run?
import Combine
var started = 0
let f = Future<Int, Never> { p in started += 1; p(.success(1)) }
print("started", started)
- started 0
- started 1
Mental model. A publisher is a recipe card. Nothing is cooked until someone orders (subscribes), and each order cooks a fresh dish. The subscription is the order ticket with “send me two more” and “cancel”.
A Publisher declares an Output type and a Failure type (Never if it cannot fail) and one method, receive(subscriber:). A Subscriber has matching Input and Failure types and three callbacks: receive(subscription:), receive(_ input:) -> Demand and receive(completion:). The Subscription is the live connection created when they meet: it carries request(_ demand:) (the subscriber asks for more values) and cancel(). The handshake is: subscriber attaches, the publisher creates a subscription and hands it over, the subscriber requests demand, the publisher sends values (never more than requested), and finally exactly one completion (.finished or .failure(error)), after which nothing more is delivered. Output and failure types must match, which is why operators like setFailureType and mapError exist.
The key property is that publishers are lazy and cold. A chain such as [1, 2, 3].publisher.map { … } only builds a description; no work runs and no side effects happen until a subscriber attaches (sink, assign, or your own), and each subscriber gets its own run of the recipe (so two sinks on a request publisher make two requests, unless you add share()). Most publishers deliver synchronously on subscription when the data is already there (an array publisher or Just delivers all its values, then completes, before sink even returns). Operators are themselves publishers wrapping the upstream, so a pipeline is a nested value of generic types (the reason for eraseToAnyPublisher() at API boundaries, which hides the concrete type). Future is the exception to laziness: its closure runs when it is created, once, and later subscribers get the stored result, so wrap it in Deferred to make it lazy.
Nothing runs while the pipeline is only a description. The subscription, and its side effect, happen when the sink attaches.
Values arrive first, then a single completion, and everything has already happened by the time sink returns.
A Future runs its work eagerly when it is created, not on subscription.
Common traps
- Building a pipeline and never subscribing. Nothing runs; no request is made.
- Assuming
Futureis lazy. It starts at creation and runs once; useDeferred { Future { … } }for per-subscriber work.
They might follow up with
- How many completions can a subscription deliver? At most one, and no values after it.
- Why
eraseToAnyPublisher()? The nested generic type of a long pipeline leaks implementation details; erasing gives a stable type.
Related questions
- 11.1.2 What is demand and backpressure in Combine, and how does a custom subscriber control it?
- 11.3.1 What is the difference between PassthroughSubject and CurrentValueSubject, and when do you use a subject at…
- 3.3.5 How do Combine subscriptions create cycles, and why is assign(to:on:) a trap?
What is demand and backpressure in Combine, and how does a custom… →