Kodokon kodokon.com

Async in Dart: Future, async/await, FutureBuilder

Master Future, async/await, and FutureBuilder without blocking the UI or accidentally re-firing your requests.

10 min · 3 questions

Open this lesson in Kodokon

Dart runs your application on a single thread, driven by an event loop. A Future is not a thread: it's the promise of a value to come. await never blocks that thread - it suspends the current function, hands control back to the loop (the UI keeps running), then resumes when the result arrives. Corollary: async/await parallelizes nothing for pure computation; for CPU-intensive work, you need an isolate.

DART
Future<double> fetchRate() async {
  await Future<void>.delayed(
    const Duration(milliseconds: 300),
  );
  return 1.08;
}

Future<void> main() async {
  try {
    final rate = await fetchRate();
    print('EUR/USD rate: $rate');
  } on Exception catch (error) {
    print('Rate loading failed: $error');
  }
}
An async function is consumed with await and try/catch.

Two successive awaits run serially: the second only starts after the first. When the operations are independent, launch them together with Future.wait. The trade-off to know: Future.wait is fail-fast - the first error rejects the whole thing and the results of the other futures are lost, even if they succeed. If you need partial results, handle the error inside each future.

DART
Future<String> fetchProfile() async => 'profile';

Future<String> fetchOrders() async => 'orders';

Future<void> main() async {
  final [profile, orders] = await Future.wait(
    [fetchProfile(), fetchOrders()],
  );
  print('$profile / $orders');
}
Parallelization with Future.wait and Dart 3 destructuring.

On the UI side, FutureBuilder connects a Future to build via a snapshot. The number-one pitfall in the field: creating the future directly in build. Every rebuild - a simple setState from a parent is enough - would then re-fire the request. Store the future in initState and always test for error before data.

DART
import 'package:flutter/material.dart';

class ProfileScreen extends StatefulWidget {
  const ProfileScreen({super.key});

  @override
  State<ProfileScreen> createState() =>
      _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  late final Future<String> _nameFuture;

  @override
  void initState() {
    super.initState();
    _nameFuture = _loadName();
  }

  Future<String> _loadName() async {
    await Future<void>.delayed(
      const Duration(seconds: 1),
    );
    return 'Ada Lovelace';
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _nameFuture,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        if (!snapshot.hasData) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
        return Text(snapshot.data!);
      },
    );
  }
}
The future is created only once, in initState.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What does await actually do on the main thread?
    • It blocks the thread until the Future resolves.
    • It automatically moves the computation onto a secondary thread.
    • It suspends the current function and hands control back to the event loop: the UI keeps running.
  2. With Future.wait([a(), b()]), what happens if a() fails?
    • Future.wait rejects with the error from a(); the result of b() is lost.
    • Future.wait waits for b() to finish, then returns its result alone.
    • The error is ignored as long as b() succeeds.
  3. Why avoid future: fetchUser() written directly in build with FutureBuilder?
    • build can't contain an asynchronous call, the code doesn't compile.
    • Every rebuild would create a new Future, and therefore re-fire the network request.
    • The snapshot would stay stuck in a waiting state.