How to Cancel a Dart Future

Once in a while, you need to cancel a Future in Dart code. I've seen a bit of discussion about this online, but it's not that easy to discover a solution, so I thought I'd improve that situation a bit by detailing one possible approach.

This code was tested with Dart SDK v2.0.0.

The Problem

Let's say your app makes a call to a REST service, and when that service returns data, a Future is completed and your display updates in response. This situation might look something like the following:

main() async {
  List data = await getData();
  updateDisplay(data);
}

Future<List> getData() async {
  // make the REST call
  Response restResponse = await HttpRequest.get(.....);
  
  // massage the data
  List data = restResponse.response.map((Map record) {
    // transform/check raw records
    // return good data
  });
  
  return data;
}

void updateDisplay(List data) {
  // update the display
}

Obviously, that's not real code, but you get the idea.

Now, what if that REST call takes a long time? What if your user becomes impatient and navigates away from the view that required the data in the first place? Wouldn't it be nice if there was a way to cancel the Future you're awaiting on the first line of main()? You don't want to update the display with that data anymore, and you shouldn't have to accept it!

The Solution

Many of you know that you can't cancel a Future in Dart, but you can cancel a subscription to a Stream. So one way you could handle this situation is to rewrite getData() to return a Stream instead. That may or may not be possible or desirable.

Fortunately, you can easily convert a Future to a Stream from the caller:

void main() {
  // keep a reference to your stream subscription
  StreamSubscription<List> dataSub;
  
  // convert the Future returned by getData() into a Stream
  dataSub = getData().asStream().listen((List data) {
    updateDisplay(data);
  });

  // user navigated away!
  dataSub.cancel();
}

Using Future's asStream() method, you can essentially listen to a Future as though it were a Stream (which is what it becomes behind the scenes). That way, you can save your subscription and use it to cancel your callback if you find you no longer need the data that has yet to arrive. All this without having to modify the getData() function. Pretty nifty!

What's Really Happening?

It's important to keep in mind that it isn't actually possible to prevent a Future from completing. All you can do is stop your callback from executing under particular circumstances, such as if the response becomes irrelevant before it comes back. In the code example, calling dataSub.cancel() before the Future completes achieves this.

Another Angle

Before we close out this subject, let's take a look at a slightly more realistic arrangement of code making use of this technique:

// keep a reference to your stream subscription
StreamSubscription<List> dataSub;

void onGetDataClicked() {
  // make sure any still-pending prior request never gets processed
  dataSub?.cancel();
  
  // convert the Future returned by getData() into a Stream
  dataSub = getData().asStream().listen((List data) {
    updateDisplay(data);
  });
}

With this flow, you can be sure only the latest data request ends up on your display.

Are There Any Other Options?

There are! You could use a CancelableOperation from the async package, but that's a tale for another post.