If you are new to Flutter you might have difficulties working with async code. For some people, this might even be the first time seeing anything like async code. This article should help you grasp basic concepts about Future in Flutter and make you a proficient async programmer.

Sync vs. Async programming

Imagine two different kinds of communication: in-person talk and email correspondence. When you talk with somebody who stands right in front of you there is hardly any delay. You ask something and your peer immediately responds. And importantly, you wait for the other person to answer. This is synchronous communication.

On the other hand, when you write an email you might not hear back right away. You may receive a reply after a few hours, days, or in some cases, never. Unlike with the in-person talk you expect the delay when you chat on email. You don’t sit idly while waiting for a reply. You do some different tasks in the meanwhile and return when the email arrives. This is asynchronous communication.

And this is the point with async programming. Some operations are instant and you are supposed to wait for them to finish. However, other operations (like network requests) might take unknown time to end. If your program waits for them it cannot do anything else in the meantime. This would appear to the user as unresponsiveness or even lags.

The problem is that the compiler does not know which operations (i.e. methods or functions) are instant and which are not. It needs to handle them differently. Hence you need to need to tell the compiler (or interpreter) exactly which operations should be awaited and which should not.

async keyword

Dart’s way of telling the compiler about async operations is using the async keyword.

Lets start with simple synchronous code. Below you have a main function and fetchNumber function which fetches some number in 3 seconds. This example does not use async and therefore the compiler will wait for each operation.

import 'dart:io';

void main() {
  print(fetchNumber());
  print('Hello world!');
}

int fetchNumber() {
  print('Starting to sleep for 3 seconds!');
  sleep(Duration(seconds: 3));
  return 1337;
}

Because fetchNumber is not marked as async and because there is a sleep for 3 seconds, every other operation like the print('Hello world) will be delayed.

You need two things to make a function async: mark it as async and wrap the return type in Future.1 async tells the compiler that this function is asynchronous. Future, however, deserves a chapter on its own.

Into the Future

Future<T> represents a T value you ought to receive in the future. You can for example have Future<int>, which means that you expect to receive an int value (or an error).

Future can be in 3 different states: in progress, done with value and done with error. Future is by default in progress which means that no value or error is yet available.

Because you don’t know exactly when Future<T> finishes with either value T or an error, you need a way to listen to its changes. This might be done using callbacks. Callbacks are a special way of registering a method to be called after some time or event. In the case of Future the callback method is then() and will be called after Future finishes with value. Lets change the previous example to use a callback.

void main() {
  fetchNumber().then((value) => print(value));
  print('Hello world!');
}

Future<int> fetchNumber() async {
  print('Starting to sleep for 3 seconds!');
  return Future.delayed(Duration(seconds: 3), () => 1337);
}

There are more callback methods for the Future type.2 You can use catchError() callback to handle a situation when Future finishes with an exception. And in some cases, you can apply timeout() callback to prevent stuck Future and return own value when given time passes.

await keyword

When you overuse callback you might end up in a special place called callback hell. You often need to work with multiple async methods at the time and nesting multiple callbacks make code quickly unreadable. For cases when you want to keep your code tidy there is a better choice: await. The keyword await lets you work with async calls synchronously. Using await you can work with async code the same way you work with sync code.

Future<void> main() async {
  print(await fetchNumber());
  print('Hello world!');
}

Future<int> fetchNumber() async {
  print('Starting to sleep for 3 seconds!');
  await Future.delayed(Duration(seconds: 3));
  return 1337;
}

// Starting to sleep for 3 seconds!
// 1337
// Hello world!

When you put the await keyword before an async call you tell the compiler that you want only its value T. The program will stop at the given line and wait for the Future to finish. Note that you can only use await inside methods marked as async.

You should note that await changes type signature. Whenever you put await before an async method you will only receive the final value T.

Future<int> fetchNumber() async {
  return Future.delayed(Duration(seconds: 2), () => 10);
}

// Type of `result` is Future<int>. Compiler will not pause at line below.
var result = fetchNumber();

// Type of `awaitedResult` is int. Compiler will pause at this line.
var awaitedResult = await fetchNumber();

Using await allows you to use regular try/catch statements and catch possible exceptions without needing callbacks. This significantly helps when working with async operations.

Future<void> main() async {
  try {
    print(await fetchNumber());
  } catch (e) {
    print('Error: $e');
  }

  print('Hello world!');
}

Future<int> fetchNumber() async {
  throw 'An exceptional exception!';
  return 1337;
}

// Error: An exceptional exception!
// Hello world!

Useful lints

Bugs caused by async code are one of the hardest to debug. I often forget to await futures and then its only matter of when something breaks. This is easily prevented using unawaited_futures lint rule. Whenever you call an async method in async context (inside a different async method) and forget to put await you will get a warning.

Create a file named analysis_options.yaml at your project’s root (next to the pubspec.yaml) and put the following code inside.

linter:
  rules:
    - unawaited_futures

Another nice lint is unnecessary_await_in_return. When you return some Future value from a method you don’t have put await before. In the example below when you call the fetchNumber you will need to await its returned Future. Because the first fetchNumber is marked async the returned value will always be wrapped inside the Future and therefore any await inside is redundant.3

// DON'T
Future<int> fetchNumber() async {
  return await Future.delayed(Duration(seconds: 3), () => 1337);
}

// DO
Future<int> fetchNumber() {
  return Future.delayed(Duration(seconds: 3), () => 1337);
}

Async Widgets

You have an async call that fetches some data and want to display it in a Flutter app. The easiest way to do this is with the FutureBuilder widget. However, we can create our own version to see how it works.

Futures can be in different states: loading, done, or error. Therefore you need to use StatefulWidget and change state accordingly.

class AsyncBuilder extends StatefulWidget {
  @override
  _AsyncBuilderState createState() => _AsyncBuilderState();
}

class _AsyncBuilderState extends State<AsyncBuilder> {
  // `int` value we expect to receive. When fetching is in progress
  // this will be null.
  int value;

  @override
  void initState() {
    super.initState();

    // Add callback to async `fetchNumber` call. You cannot use `await`
    // because `initState` is not async.
    fetchNumber().then((result) {

      // Once we receive our number we trigger rebuild.
      setState(() {
        value = result;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    // When value is null show loading indicator.
    if (value == null) {
      return const CircularProgressIndicator();
    }

    return Text('Fetched value: $value');
  }
}

This example does not handle possible exceptions. You might accomplish this by adding a new variable called error (with dynamic type). In case the Future throws you save the error into the error variable. Then inside the build method check if error != null and show some message.

As I mentioned few paragraphs above you will most of the time use the FutureBuilder widget from Flutter. It does everything as the example above (and probably better).

FutureBuilder(
  future: fetchNumber(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text('Fetched value: ${snapshot.data}');
    } else if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    } else {
      return CircularProgressIndicator();
    }
  },
)

There is, however, one important caveat. FutureBuilder requires a future param. In our case, we provide future value using the fetchNumber method. Due to this each time the widget that contains FutureBuilder above will call the fetchNumber and acquire a new future value. FutureBuilder then on each rebuild checks if the params changed and updates itself. This means that the future provided might be called several times (possibly on each rebuild) which is wasteful.

To fix this we need to call fetchNumber only once inside the initState, save the returned Future<int> into a variable, and provide only the variable into the FutureBuilder. This way the equality check inside FutureBuilder works and the async call will not be repeated on rebuilds.


class _AsyncWidgetState extends State<AsyncWidget> {
  Future<int> future;

  @override
  void initState() {
    super.initState();
    future = fetchNumber();
  }

  Future<int> fetchNumber() async {
    return Future.delayed(Duration(seconds: 3), () => 1337);
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: future,
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Text('Fetched value: ${snapshot.data}');
        } else if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        } else {
          return CircularProgressIndicator();
        }
      },
    );
  }
}

Because the initState is guaranteed to be called only once in stateful widgets there will not be multiple futures created. By the way, this is the behavior of our custom AsyncBuilder widget.


  1. This is a simplification and not exactly the truth. You can omit async and only set the return type to Future. However, the async keyword will automatically set every returned value as Future. This makes writing code easier. ↩︎

  2. For complete API definition see Dart docs ↩︎

  3. Both cases work the same and this lint serves only to keep tidy codebase. ↩︎