Why use async/await instead of Future.then?

Alexey Inkin
Flutter Senior
Published in
4 min readJan 8, 2022

These two methods do almost the same:

They get some objects from what could be a network call, wait for the result, then get the title of each object, and handle possible errors if the awaited operation fails.

The first one is the old way, and it is called ‘The Future API’. You may still find it in old tutorials. The second one is the modern way, and it is called ‘Native Asynchrony’. It relies on the same Future API under the hood but wraps it with a cleaner syntax.

You should not use the Future API directly for the following reasons:

Exceptions are not handled before the gap

Imagine the repository implementation like this:

It has 2 parts:

  1. _prepare() is called synchronously before getObjects() returns.
  2. _parseObjects(map) is called when the raw data arrives from the network.

What happens between them is called an ‘asynchronous gap’. Essentially it means that getObjects() returns a future, and at some later point the callback in then runs.

catchError in the outer code gives a false sense of safety, but it only handles exceptions in _parseObjects(). This is just because catchError is a method that is attached to whatever getObjects() returns. If it does not return normally, catchError cannot attach to it.

Follow these links to run two snippets with exceptions before and after the gap in your browser.

So, for the two methods to be equivalent, the first one needs two identical error handlers:

Note that this problem only happens if the throwing function is synchronous. If the called function is async, then throw starts the gap, and exceptions from there will be caught even before any await:

But the calling code cannot rely on the function being async, this can change any time. So this brings uncertainty instead of seeming safety.

Harder to set breakpoints

With most IDEs, you cannot set a breakpoint inside your then callback function unless you break its body into a separate line:

But it is hard to set breakpoints in lines like this:

.then((articles) => articles.map((article) => article.title)),

Harder to move code across the gap

If you need to move doSomething() after the objects arrive, you only have messy options. You may expand your then callback to multiple lines (1). Or you may wrap it into a separate then (2). Compare this to just moving the line in native asynchrony.

Easy to lose track of the results

In the snippet above, then on line 3 receives the result from getObjects(), while then on line 7 receives the result from the previous then. Imagine you need to add yet another then after that, and access articles there. You will have to update every earlier callback before it to return its argument. This is redundant and easy to lose track of.

Callbacks are dirty

  • A callback breaks the sequence of execution. For humans, it is harder to follow a function with nested functions.
  • A callback repeats identifiers too much. You definitely want fewer articles here:
    .then((articles) => articles.map((article) => article.title)),
    You want this:
    _titles = articles.map((article) => article.title);
  • A callback uses one arrow operator. If you add one to yourself, it makes two as in the example. Two arrow operators in one line are harder to follow.
  • A callback uses extra (parentheses) and possibly {braces}. Asynchronous code does not.
  • A callback body is indented. Each indentation is an obstacle when reading. Each indentation takes 2 characters of your 80 characters limit that the Dart style guide recommends.

In a few words, callbacks are boilerplate around what you really need to run. Eliminate the boilerplate.

Anonymity gives no hints

With native asynchrony, you write the result into a separate variable as it arrives, in this case:

final articles = await ...

… and its name becomes a hint. This way it is clear what the objects are. Sure, in the old way, you can hint the object type on the next line with .map((article) => ..., but the reader still needs to grasp two lines for the picture. With native asynchrony, each line makes sense on its own.

Harder to format

The Future API gives you a chain of methods in one expression spanning multiple lines. In many cases, the Dart formatter will mess it up. On the other hand, with native asynchrony you mostly have short lines which are OK to the formatter.

Harder to switch between sync and async

Suppose getObjects() stopped being asynchronous. Drop await, and you’re good. But with Future API you need to dismantle the whole .then and .catchError thing.

Or suppose you had it the synchronous way from the beginning. And then getting the objects became asynchronous. You may just add async and await, and you got it. But if you are only accustomed to the old way, you would have a lot of refactoring wrapping it into then-catchError chains.

Learn native asynchrony

So these are not equal options. Go ahead and learn async-await here:

Use cases for the Future API

I only remember one case where I needed to use then:

Before saving some data, we need to validate it, and the validation is asynchronous. If valid, the data is saved. But we need to return both futures so the outer code can show different progress indicators for the two operations.

In this case, we cannot await the first future because then we would have to return a single future from _onSavePressed which is not what we need.

One way around this is to pass validationFuture into _saveIfValid() and await it there like this:

But that adds a new responsibility of awaiting to _saveIfValid which is totally alien there. I went with a simple then.

So go ahead and learn the old Future API as well:

--

--

Flutter Senior
Flutter Senior

Published in Flutter Senior

A knowledge base to my company. We use this to teach juniors and link here in reviews. And so can you. Everything is heavily opinionated here except that Flutter and the related logo are trademarks of Google LLC. We are not endorsed by or affiliated with Google LLC.

Alexey Inkin
Alexey Inkin

Written by Alexey Inkin

Google Developer Expert in Flutter. PHP, SQL, TS, Java, C++, professionally since 2003. Open for consulting & dev with my team. Telegram channel: @ainkin_com