Skip to content

Save that sink! A simple solution to a common Combine problem

So you’re starting to work with Combine, because Apple is finally jumping on the reactive programming bandwagon (yay!). So you build your first Combine workflow, and it looks something like this:

func retrieveData() {
    URLSession.shared.dataTaskPublisher(for: url)
      .map({$0.data})
      .eraseToAnyPublisher()
      .sink(receiveCompletion: { (status) in
        switch status {
        case .failure(let error):
          print(error.localizedDescription)
        case .finished:
          break
        }
      })
      { (data) in
        let str = String(data: data, encoding: .utf8)
        print(str!)
      }
}

And then you try and use it, and instead of getting the data response you were expecting, you get an error:

2019-10-25 14:59:34.452071-0400 TestApp[2127:98883] Task <663D6D3A-48B8-49E6-9103-AA1D89513D84>.<1> finished with error [-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled" …

Cancelled? Why did my request get cancelled?

The key in the above code is that you missed something important. I made this exact same mistake when I started using Combine, and sadly I don’t think it will be the last time, because it is just that easy to miss. Let’s talk about the solution, and why you get the error when you don’t do it.


Let’s get the solution out of the way first: you need to save the result of that long Combine chain into a variable of type AnyCancellable, somewhere outside of the retrieveData() method, so that it gets retained and not cleaned up by memory management:

var cancellable: AnyCancellable?

func retrieveData() {
  cancellable = URLSession.shared.dataTaskPublisher …
}

You should do this for any subscriber you create, either via .sink(), .assign(), or manually-constructred Subscriber. Let’s dig into why you get the error when you don’t do this.

Any time you connect a Subscriber to a Combine Publisher, either via the automatic .sink() or .assign() operator methods, or via .subscribe() on a manually-constructed Subscriber, you get back an object is an AnyCancellable object. This object allows you to later cancel the subscription later on.

But what happens if you don’t save that AnyCancellable?

You probably just made the mistake of not storing it away, either because you didn’t realize that those calls returned something, you thought that you didn’t need to save that result for things to work, or you just forgot (it’s easy to do). But at that point, ARC memory management takes over.

Because you didn’t save a reference to that AnyCancellable, ARC sees there are no references, and does its job: cleans it up for you. Standard Combine subscribers are written to have a deinit() method which will cancel their subscription when they are deallocated. When that happens, that cancel propagates back through your Publisher chain, back to your dataTaskPublisher. It receives the cancel, and stops your URL query before completion, which then sends a failure back down your Publisher chain and back to your Sink as an error, which gets printed in the .failure block in the sink.

This Combine chain returns an obvious error, because of the dataTaskPublisher getting cancelled. Other Combine chains may not be so obvious: they may just do nothing, return no data, not do some of their steps, etc. This becomes a really hard to debug problem, which you’ll be kicking yourself for if this simple solution is the fix all along.

So if your Combine chains seem to be not doing the work they should be, one of the first things to check is to remember to save that sink!

Comments are closed.