flutter
/

Flutter Future – Asynchronous Operations Made Easy

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Flutter Future – Asynchronous Operations Made Easy

What is a Future?

A Future represents a potential value or error that will be available at some time in the future. It's the core of asynchronous programming in Flutter. When you perform a time‑consuming operation (like a network request, file I/O, or database query) you don't want to block the UI. Instead, you get a Future that will complete when the operation finishes, and you can continue executing other code in the meantime.

Creating a Future

There are several ways to create a Future:

    • Future.value – creates a future that completes immediately with a value.
    • Future.error – creates a future that completes immediately with an error.
    • Future.delayed – creates a future that completes after a delay.
    • Future.sync – runs a function synchronously and completes with its result.
    • async functions – any function marked async automatically returns a Future.
DARTRead-only
1
// Immediate value
Future<int> immediate = Future.value(42);

// Immediate error
Future<String> error = Future.error('Something went wrong');

// Delayed completion
Future<String> delayed = Future.delayed(
  Duration(seconds: 2),
  () => 'Data loaded',
);

// Async function
Future<int> fetchNumber() async {
  await Future.delayed(Duration(seconds: 1));
  return 42;
}

Using then, catchError, and whenComplete

The traditional way to handle a Future is with .then() for success, .catchError() for errors, and .whenComplete() for cleanup. However, async/await is now preferred for readability.

DARTRead-only
1
fetchUserData().then((data) {
  print(data);
}).catchError((error) {
  print('Error: $error');
}).whenComplete(() {
  print('Done');
});

Async/Await – The Modern Way

async and await make asynchronous code look like synchronous code. Mark a function as async, then use await to wait for a Future to complete. The await expression returns the result of the Future. Errors are caught with try/catch.

DARTRead-only
1
Future<void> loadData() async {
  try {
    String data = await fetchData();
    print(data);
  } catch (e) {
    print('Error: $e');
  }
}

FutureBuilder – Reactive UI for Futures

FutureBuilder is a Flutter widget that builds itself based on the state of a Future. It handles the loading, error, and data states for you. You provide a future and a builder that receives a snapshot containing the current connection state, data, and error.

DARTRead-only
1
FutureBuilder<String>(
  future: fetchData(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    } else if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    } else if (snapshot.hasData) {
      return Text('Data: ${snapshot.data}');
    } else {
      return Text('No data');
    }
  },
)

Handling Different Connection States

The snapshot's connectionState tells you the status:

    • ConnectionState.none: No future provided or the future has not started.
    • ConnectionState.waiting: The future is still pending (loading).
    • ConnectionState.active: For streams, but for futures it's rarely used.
    • ConnectionState.done: The future has completed (success or error).

You can check snapshot.hasData, snapshot.hasError, and snapshot.data to decide what to show.

Combining Multiple Futures

Sometimes you need to wait for several futures to complete. Use Future.wait to run them in parallel and wait for all to complete. It returns a future that completes with a list of results.

DARTRead-only
1
Future<String> fetchUser() async => 'User';
Future<String> fetchPosts() async => 'Posts';

Future<void> loadAll() async {
  var results = await Future.wait([fetchUser(), fetchPosts()]);
  print(results[0]); // User
  print(results[1]); // Posts
}

Future.any returns the first future that completes (success or error).

DARTRead-only
1
var first = await Future.any([fetchFromServerA(), fetchFromServerB()]);

Error Handling in Futures

Unhandled errors in a Future can cause crashes. Always handle errors:

    • With async/await: use try/catch.
    • With .then: use .catchError.
    • With FutureBuilder: check snapshot.hasError and show a user‑friendly message.
DARTRead-only
1
FutureBuilder(
  future: riskyOperation(),
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return Text('Something went wrong. Please try again.');
    }
    // ...
  },
)

Best Practices

    • Store the future in the state: Avoid creating the future inside the build method because it will be recreated on every rebuild. Create it in initState or as a variable.
    • Use FutureBuilder for UI: It manages the subscription and state for you.
    • Handle errors gracefully: Show a user‑friendly message, not just a red screen.
    • Consider cancellation: If a future is no longer needed (e.g., widget disposed), consider canceling it. The future parameter in FutureBuilder is not automatically canceled; if the future can't be canceled, use a StatefulWidget and manage the flag.

Common Mistakes

    • Recreating the future in build: Causes the future to restart on every rebuild, leading to flickering and performance issues.
    • Not checking connectionState: Showing a loading indicator only when snapshot.hasData is false can lead to showing stale data while loading.
    • Forgetting to handle errors: Uncaught errors may crash the app.
    • Using setState with futures: FutureBuilder is cleaner and avoids manual state management.

Complete Example

DARTRead-only
1
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('FutureBuilder Demo')),
        body: Center(child: UserProfile()),
      ),
    );
  }
}

class UserProfile extends StatefulWidget {
  @override
  _UserProfileState createState() => _UserProfileState();
}

class _UserProfileState extends State<UserProfile> {
  late Future<String> _userFuture;

  @override
  void initState() {
    super.initState();
    _userFuture = fetchUser();
  }

  Future<String> fetchUser() async {
    await Future.delayed(Duration(seconds: 2));
    return 'John Doe';
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _userFuture,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        } else if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        } else if (snapshot.hasData) {
          return Text('Welcome, ${snapshot.data}!');
        } else {
          return Text('No user data');
        }
      },
    );
  }
}

Key Takeaways

    • Future represents an asynchronous operation that will complete later.
    • Use async/await for clean asynchronous code.
    • FutureBuilder builds UI based on future state (loading, error, data).
    • Combine multiple futures with Future.wait or Future.any.
    • Always handle errors to prevent crashes.
    • Store the future outside build to avoid unnecessary recreation.

Try it yourself

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Future Demo')),
        body: Center(child: DataLoader()),
      ),
    );
  }
}

class DataLoader extends StatefulWidget {
  @override
  _DataLoaderState createState() => _DataLoaderState();
}

class _DataLoaderState extends State<DataLoader> {
  late Future<String> _future;

  @override
  void initState() {
    super.initState();
    _future = fetchData();
  }

  Future<String> fetchData() async {
    await Future.delayed(Duration(seconds: 2));
    return 'Data loaded successfully!';
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _future,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        } else if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        } else if (snapshot.hasData) {
          return Text(snapshot.data!);
        } else {
          return Text('No data');
        }
      },
    );
  }
}

Test Your Knowledge

Q1
of 4

What is the purpose of `FutureBuilder` in Flutter?

A
To create a future
B
To display a widget based on the state of a future
C
To handle errors in async code
D
To cancel a future
Q2
of 4

How do you wait for multiple futures to complete simultaneously?

A
Future.any
B
Future.wait
C
Future.all
D
Future.join
Q3
of 4

What is the correct way to create a `Future` that completes after 2 seconds?

A
Future(seconds: 2, () => 'done')
B
Future.delayed(Duration(seconds: 2), () => 'done')
C
Future.timeout(Duration(seconds: 2), () => 'done')
D
Future.wait(Duration(seconds: 2))
Q4
of 4

In a `FutureBuilder`, which `ConnectionState` indicates the future is still pending?

A
none
B
waiting
C
active
D
done

Frequently Asked Questions

What is the difference between `FutureBuilder` and using `setState` with a future?

FutureBuilder automatically manages the subscription and rebuilds when the future completes. It also handles the loading and error states elegantly. Using setState manually requires you to track the state yourself and may cause unnecessary rebuilds. FutureBuilder is the preferred way for UI that depends on a future.

How do I cancel a future if the widget is disposed?

Futures themselves are not cancelable. However, you can use a StatefulWidget and a boolean flag to ignore the result if the widget is no longer mounted. For example: if (mounted) setState(...). If you need true cancellation, use a Stream or a package like cancelable_future.

Can I use `FutureBuilder` with a `Future` that returns `null`?

Yes, but snapshot.hasData will be false when the data is null. You can check snapshot.data != null if you need to distinguish null from no data, but usually you'd treat null as a valid value. Better to use FutureBuilder with a non‑nullable type and handle loading/error states separately.

What is the difference between `Future.wait` and `Future.any`?

Future.wait waits for all futures to complete and returns a list of results. It fails if any future fails. Future.any returns the result of the first future that completes (whether success or error).

Why does my `FutureBuilder` keep rebuilding?

If you create the future inside the build method, it will be recreated on every rebuild, causing the FutureBuilder to restart the future. Move the future creation to initState or keep it as a variable outside the build method.

Previous

flutter async await

Next

flutter stream

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.