Let’s take a closer look at the Future API, some of which you just saw in use.

OK, first question: how do you get an instance of a Future ? Most of the time, you don’t create futures directly. That’s because many of the common asynchronous programming tasks already have libraries that generate futures for you.

For example, network communication returns a future:

final myFuture = http.get('http://example.com');

Getting access to shared preferences also returns a future:

final myFuture = SharedPreferences.getInstance();

But you can also use Future constructors to create futures.

Future constructors

The simplest constructor is Future() , which takes a function and returns a future that matches the function’s return type. Later the function runs asynchronously, and the future completes with the function’s return value. Here’s an example of using Future() :

Let’s add a couple of print statements to make the asynchronous part clear:

If you run that code in DartPad (dartpad.dev), the entire main function finishes before the function given to the Future() constructor. That’s because the Future() constructor just returns an uncompleted future at first. It says, “Here’s this box. You hold onto that for now, and later I’ll go run your function and put some data in there for you.” Here’s the output of the preceding code:

Done with main().

Creating the future.

Another constructor, Future.value() , is handy when you already know the value for the future. This constructor is useful when you’re building services that use caching. Sometimes you already have the value you need, so you can pop it right in there:

final myFuture = Future.value(12);

The Future.value() constructor has a counterpart for completing with an error. It’s called Future.error() , and it works essentially the same way, but takes an error object and an optional stacktrace:

final myFuture = Future.error(ArgumentError.notNull('input'));

The handiest future constructor is probably Future.delayed() . It works just like Future() , except that it waits for a specified length of time before running the function and completing the future.

One way to use Future.delayed() is when you’re creating mock network services for testing. If you need to make sure your loading spinner displays correctly, a delayed future is your friend.

Using futures

Now that you know where futures come from, let’s talk about how to use them. As we mentioned earlier, using a future is mostly about accounting for the three states it can be in: uncompleted, completed with a value, or completed with an error.

The following code uses Future.delayed() to create a future that completes after 3 seconds with a value of 100.

When this code executes, main() runs from top to bottom, creating the future and printing “Waiting for a value…” That whole time, the future is uncompleted. It doesn’t complete for another 3 seconds.

To use the completed value, you can use then() . That’s an instance method on each future that you can use to register a callback for when the future completes with a value. You give it a function that takes a single parameter matching the type of the future. Once the future completes with a value, your function is called with that value.

Here’s the output of the preceding code:

Waiting for a value... (3 seconds pass until callback executes)

The value is 100.

In addition to executing your code, then() returns a future of its own, matching the return value of whatever function you give it. So if you need to make a couple of asynchronous calls, you can chain them together even if they have different return types.

Back to our first example, what happens if that initial future doesn’t complete with a value — what if it completes with an error? The then() method expects a value. You need a way to register another callback in case of an error.

The answer is to use catchError() . It works just like then() , except that it takes an error instead of a value, and it executes if the future completes with an error. Just like then() , the catchError() method returns a future of its own, so you can build a whole chain of then() and catchError() methods that wait on one another.

Note: You don’t need to call then() or catchError() if you use the async-await language feature. Instead, you await the completed value, and you use try-catch-finally to handle errors. For details, see the Dart language tour’s asynchrony support section.

Here’s an example of using catchError() to handle the case where a future completes with an error:

You can even give catchError() a test function to check the error before invoking the callback. You can have multiple catchError() functions this way, each one checking for a different kind of error. Here’s an example of specifying a test function, using the optional test parameter to catchError() :

Now that you’ve gotten this far, hopefully you can see how the three states of a future are often reflected by the structure of the code. There are three blocks in the preceding example:

The first block creates an uncompleted future. Then there’s a function to call if the future completes with a value. Then there’s another function to call if the future completes with an error.

There’s one more method you might want to use: whenComplete() . You can use it to execute a function when the future is completed, no matter whether it’s with a value or an error.

It’s kind of like the finally block in a try-catch-finally. There’s code executed if everything goes right, code for an error, and code that runs no matter what.

Using futures in Flutter

So that’s how you create futures, and a bit about how you can use their values. Now let’s talk putting them to work in Flutter.

Say you have a network service that’s going to return some JSON data, and you want to display that data. You could create a StatefulWidget that creates the future, checks for completion or error, calls setState() , and generally handles all the wiring manually.

Or you can use FutureBuilder . It’s a widget that comes with the Flutter SDK. You give it a future and a builder function, and it automatically rebuilds its children when the future completes.

The FutureBuilder widget works by calling its builder function, which takes a context and a snapshot of the current state of the future.

You can check the snapshot to see whether the future completed with an error:

Otherwise you can check the hasData property to see if it completed with a value:

If neither hasError nor hasData is true, then you know you’re still waiting, and you can output something for that as well.

Even in Flutter code, you can see how those three states keep popping up: uncompleted, completed with value, and completed with error.

Summary

This article talked about what futures represent and how you can use the Future and FutureBuilder APIs to create futures and use their completed values.

If you’d like to learn more about using futures—with the option of using runnable examples and interactive exercises to test your understanding—check out the asynchronous codelab on futures, async, and await.

Or go on to the next video in the Asynchronous Programming in Dart series. It talks about streams, which are a lot like futures in that they can provide either values or errors. But where futures just give you one result and stop, streams just keep right on going.

Big thanks to Andrew Brogdon, who created the video that this article is based on.