Futures and async/await

Dart · Practical · 14 min

Every network call, file read and database query in Dart and Flutter returns a Future.

A `Future<T>` represents a value (or error) that will be available later. Functions that do I/O return futures instead of blocking, because Dart runs your code on a single thread per isolate.

Mark a function async and use await to wait for a future without blocking:

Future<User> loadUser(int id) async {
  final json = await api.get('/users/$id');
  return User.fromJson(json);
}

An async function always returns a Future, even if the body returns a plain value. Use Future<void> when nothing is returned. Errors from awaited futures are thrown at the await, so ordinary try/catch/finally works.

Important behaviors:

  • An async function runs synchronously until its first await, then returns the future to its caller.
  • Sequential awaits run one after another. For independent work, start the futures first and wait together: await Future.wait([a(), b()]), or with records final (u, p) = await (loadUser(), loadPosts()).wait;.
  • .timeout(Duration(...)) fails a slow future with a TimeoutException.
  • A future you forget to await runs anyway, but its errors become uncaught. Enable the unawaited_futures lint and wrap intentional fire-and-forget calls in unawaited(...).

Future.delayed, Future.value and Completer create futures yourself; the latter bridges callback-based APIs.

Example

import 'dart:async';
Future<String> fetch(String what, int ms) async {
  await Future.delayed(Duration(milliseconds: ms));
  if (what.isEmpty) throw ArgumentError('empty request');
  return '$what loaded';
}
Future<void> main() async {
  final sw = Stopwatch()..start();
  final a = await fetch('user', 200);
  final b = await fetch('posts', 200);
  print('$a, $b in ~${sw.elapsedMilliseconds ~/ 100 * 100} ms'); // ~400 ms
  sw.reset();
  final (u, p) = await (fetch('user', 200), fetch('posts', 200)).wait;
  print('$u, $p in ~${sw.elapsedMilliseconds ~/ 100 * 100} ms'); // ~200 ms
  try {
    await fetch('', 10);
  } on ArgumentError catch (e) {
    print('failed: ${e.message}');          // failed: empty request
  }
  try {
    await fetch('slow', 500).timeout(const Duration(milliseconds: 50));
  } on TimeoutException {
    print('timed out');                     // timed out
  }
}

Remember

  • async functions return a Future; await unwraps it
  • Start independent futures together and wait with Future.wait or (a, b).wait
  • Errors surface at await and are caught with try/catch

Watch out for

  • Forgetting await, so errors go uncaught and code runs out of order.

Check yourself

  1. `final x = loadUser();` inside an async function, without await. What is x?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: dart.dev/libraries/async/async-await