Understanding Swift Combine's Future and Deferred Publishers

Swift's Combine framework provides powerful tools for handling asynchronous operations, and two of the most important publishers for single-event scenarios are Future and Deferred. Let's explore how these work and when to use each one.

The Future Publisher

The Future publisher in Combine represents an asynchronous operation that will eventually produce exactly one value before completing or failing. Think of it as Swift's version of a promise from JavaScript or other reactive frameworks.

What makes Future particularly useful is that it integrates seamlessly with Combine's operator chain, allowing you to transform, filter, and combine it with other publishers effortlessly.

How Future Works

Apple describes Future as "a publisher that eventually produces a single value and then finishes or fails." In terms of event flow, this looks like a single emission followed by either completion or an error.

marble diagram complete and error

Creating a Future Publisher

Let's build a simple Future that waits 2 seconds before emitting a random integer:

Future<Int, Never> { promise in
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        let number = Int.random(in: 1...10)
        promise(.success(number))
    }
}

The Future initializer accepts a closure that receives a "promise" function. This promise function expects a Result type that matches your Future's generic parameters. You call this promise with either a success value or a failure when your asynchronous work completes.

For error scenarios, you would call the promise like this:

Future<Int, SomeError> { promise in
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        promise(.failure(.networkError))
    }
}

Future's Immediate Execution

Here's something crucial to understand: Future begins executing its closure immediately upon creation, regardless of whether anyone has subscribed to it yet.

Let's demonstrate this with some logging:

let futurePublisher = Future<Int, Never> { promise in
    print("๐Ÿ”ฎ Future started working")
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        let number = Int.random(in: 1...10)
        print("๐Ÿ”ฎ Future generated: \(number)")
        promise(.success(number))
    }
}.print("๐Ÿ” Event")

Running this code produces:

๐Ÿ”ฎ Future started working
๐Ÿ”ฎ Future generated: 5

Notice that the future begins processing immediately, even without any subscribers. However, without a subscriber, the generated value goes nowhere.

When we add a subscriber:

futurePublisher
    .sink { value in
        print("๐ŸŽง Received: \(value)")
    }
    .store(in: &cancellables)

Now we see the complete flow:

๐Ÿ”ฎ Future started working
๐Ÿ” Event: receive subscription: (Future)
๐Ÿ” Event: request unlimited
๐Ÿ”ฎ Future generated: 5
๐Ÿ” Event: receive value: (5)
๐ŸŽง Received: 5
๐Ÿ” Event: receive finished

Real-World API Example

Future excels at wrapping traditional callback-based APIs. Here's how you might wrap a network request:

func fetchPosts(from url: URL) -> AnyPublisher<[Post], Error> {
    Future { promise in
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                promise(.failure(error))
                return
            }
 
            do {
                let posts = try JSONDecoder().decode([Post].self, from: data!)
                promise(.success(posts))
            } catch {
                promise(.failure(error))
            }
        }.resume()
    }
    .eraseToAnyPublisher()
}
 
fetchPosts(from: apiURL)
    .sink(
        receiveCompletion: { completion in print(completion) },
        receiveValue: { posts in print("Loaded \(posts.count) posts") }
    )
    .store(in: &cancellables)

Remember that calling fetchPosts(from:) immediately triggers the network request, even if you don't subscribe right away. Sometimes you want to delay this execution until subscription time.

The Deferred Publisher

Deferred solves the immediate execution problem by wrapping publisher creation in a closure that only runs when someone subscribes. It takes a closure that returns any publisher and calls this closure for each new subscription.

Single Subscriber Scenario

Let's wrap our previous Future example with Deferred:

let deferredPublisher = Deferred {
    Future<Int, Never> { promise in
        print("๐Ÿ”ฎ Future started working")
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            let number = Int.random(in: 1...10)
            print("๐Ÿ”ฎ Future generated: \(number)")
            promise(.success(number))
        }
    }
    .print("๐Ÿ” Event")
}

With no subscribers, this produces no output at all. The Future isn't even created until subscription occurs.

defer making the future wait

Once we add a subscriber:

deferredPublisher
    .sink { value in
        print("๐ŸŽง Received: \(value)")
    }
    .store(in: &cancellables)

We get the same output as the non-deferred version, but only after subscription.

Multiple Subscribers

Here's where Deferred shows its unique behavior. With multiple subscribers:

deferredPublisher
    .sink { print("๐ŸŽง Stream 1: \($0)") }
    .store(in: &cancellables)
 
deferredPublisher
    .sink { print("๐ŸŽง Stream 2: \($0)") }
    .store(in: &cancellables)

The output reveals something interesting:

๐Ÿ”ฎ Future started working
๐Ÿ” Event: receive subscription: (Future)
๐Ÿ” Event: request unlimited
๐Ÿ”ฎ Future started working
๐Ÿ” Event: receive subscription: (Future)
๐Ÿ” Event: request unlimited
๐Ÿ”ฎ Future generated: 3
๐Ÿ” Event: receive value: (3)
๐ŸŽง Stream 1: 3
๐Ÿ” Event: receive finished
๐Ÿ”ฎ Future generated: 8
๐Ÿ” Event: receive value: (8)
๐ŸŽง Stream 2: 8
๐Ÿ” Event: receive finished

Each subscription creates a completely new Future, resulting in separate random numbers for each subscriber.

Key Takeaways

Future acts as a promise for single asynchronous events, perfect for wrapping callback-based APIs or one-time operations. However, it begins processing immediately upon creation, which isn't always desirable.

Deferred provides lazy evaluation by deferring publisher creation until subscription occurs. When combined with Future, it ensures that expensive operations only happen when someone actually wants the result.

This combination is particularly powerful for API calls, file operations, or any scenario where you want to avoid unnecessary work until a subscriber demonstrates actual demand for the data.

Understanding these publishers helps you build more efficient reactive streams in your Swift applications, ensuring resources are used only when needed while maintaining the clean, composable nature of Combine's publisher chain.