When does a Future run its work, what do two subscribers get, and what does Deferred change?

11.8.1 · L2 · Interview

Predict first

In what order does the work run, and how many times?

import Combine

var bag = Set<AnyCancellable>()
print("1 creating Future")
let f = Future<Int, Never> { promise in print("2 Future closure runs"); promise(.success(1)) }
print("3 Future created, nobody subscribed")
let d = Deferred { Future<Int, Never> { promise in print("5 Deferred's Future runs"); promise(.success(2)) } }
print("4 Deferred created")
f.sink { print("got", $0) }.store(in: &bag)
f.sink { print("got again", $0) }.store(in: &bag)
d.sink { print("got", $0) }.store(in: &bag)
d.sink { print("got", $0) }.store(in: &bag)
  • 1, 2, 3, 4, got 1, got again 1, 5, got 2, 5, got 2
  • 1, 3, 4, got 1, got again 1, 2, 5, got 2, got 2
  • 1, 3, 4, 2, 5, got 1, got again 1, got 2, got 2

Mental model. A Future is a cake baked as soon as you place the order, then cut for whoever asks. Deferred is a kitchen that starts baking only when a customer sits down, a new cake for each customer.

A Future is eager: its closure runs when you create it, before anyone subscribes, which is why the log shows “2 Future closure runs” before “3 Future created, nobody subscribed”. It also runs once and caches the outcome: both subscribers (“got 1”, “got again 1”) receive that same result and the work is not repeated. Deferred { … } is the opposite: its closure runs once per subscription and builds a fresh publisher then, so “5 Deferred’s Future runs” appears only when the first subscriber arrives, and again for the second, giving two executions.

Choose by what you want. Use a plain Future for one-shot work you want to start immediately and share (a value that is expensive to compute and should be computed once). Use Deferred around a Future for a request that should start only on subscribe and re-run for each subscriber, and for anything you will retry (see the retry question in 11.7) or return from an API where callers expect “nothing happens until I subscribe”. A function that returns a Future directly has started the work even if the caller never subscribes, which is a classic hidden side effect and a source of duplicate requests in code reviews. Also remember a Future has no cancellation of its closure: cancelling the subscription does not stop the work.

The Future ran at creation and caches its value; the Deferred runs its Future for each subscriber.

Common traps

  • Returning a bare Future from a function that should be lazy. Calling the function starts the work.
  • Expecting cancellation to stop a Future’s work. It only stops delivery to that subscriber.

They might follow up with

  • What about Just? It emits one value and completes on subscription, with the value computed beforehand.
  • Does share() make a Deferred behave like a Future? Partly: concurrent subscribers share one run, but late ones may start another or miss it (see the share question).

Related questions

  • 11.7.4 Why does retry sometimes not retry anything, and how do Future and Deferred differ here?
  • 11.8.3 What does share() do for several subscribers, and what happens to a subscriber who arrives after the shared…
  • 11.1.1 What are Publisher, Subscriber, Subscription and Subscriber.Input/Failure, and when does a pipeline actually…
  • 11.2.3 What do debounce, throttle and removeDuplicates do, and what do share and multicast change?

What happens when you send to a subject after it has completed, and… →