
When building a Flutter application, you often need to perform operations that take some time to complete. For example, fetching data from an API, reading files, accessing a database, or loading information from Firebase.
If these operations block the main execution flow, your application may become unresponsive. This is where asynchronous programming becomes important.
Flutter async and await are Dart language features that make asynchronous programming easier to understand and manage.
In this tutorial, you’ll learn what async and await mean, how Future works, how to handle API requests, and how to use asynchronous functions in real Flutter applications with practical examples.
What Is Asynchronous Programming in Flutter?
Asynchronous programming allows your application to perform an operation without waiting for it to finish before continuing other work.
For example, imagine a Flutter application that fetches user information from an API.
The request may take two or three seconds, depending on the network connection. During this time, users should still be able to interact with the application.
Instead of blocking the application while waiting for the response, Dart allows the operation to run asynchronously.
Common asynchronous operations in Flutter include:
- Fetching data from REST APIs.
- Reading and writing files.
- Accessing local databases.
- Loading Firebase data.
- Uploading images and documents.
- Performing network requests.
- Waiting for a timer or delay.
Dart uses an event loop and asynchronous programming features to manage these operations.
Synchronous vs Asynchronous Programming
Before understanding async and await, let’s understand the difference between synchronous and asynchronous programming.
1. Synchronous Programming
In synchronous programming, statements execute one after another. The next statement waits until the current operation finishes.
Example:
void main() {
print('First');
print('Second');
print('Third');
}
Output:
First
Second
Third
Each statement executes in sequence.
2. Asynchronous Programming
In asynchronous programming, an operation can complete later without blocking the normal execution flow.
Example:
void main() {
print('First');
Future.delayed(Duration(seconds: 2), () {
print('Second');
});
print('Third');
}
Output:
First
Third
Second
Here, Future.delayed() schedules an operation to complete after two seconds. The program doesn’t wait for those two seconds before printing Third.
This makes asynchronous programming useful for network requests, database operations, and other time-consuming tasks.
What Is Future in Dart?
Before learning async and await, you should understand Future.
A Future represents a result or error that will become available at some point in the future.
For example, when you request user information from an API, the response isn’t available immediately. Dart represents the pending result using a Future.
There are two possible outcomes:
- The Future completes successfully with a value.
- The Future completes with an error.
Example:
Future<String> getUserName() {
return Future.delayed(
Duration(seconds: 2),
() => 'Rahul',
);
}
void main() {
print('Fetching user...');
getUserName().then((name) {
print('User name: $name');
});
print('Request started');
}
Output:
Fetching user...
Request started
User name: Rahul
The function returns a Future<String> because its result is a String that becomes available later.
Understanding Future Types
You can use different Future types depending on the result.
Future<String> getName() async {
return 'Rahul';
}
Future<int> getAge() async {
return 25;
}
Future<bool> isLoggedIn() async {
return true;
}
Future<void> saveData() async {
print('Data saved');
}
Future<String>returns a String later.Future<int>returns an integer later.Future<bool>returns a Boolean later.Future<void>completes without returning a meaningful value.
What Is Async in Dart?
The async keyword is used to define an asynchronous function.
When you add async to a function, Dart makes it return a Future.
Example:
Future<String> getMessage() async {
return 'Hello Flutter';
}
void main() {
getMessage().then((message) {
print(message);
});
}
Output:
Hello Flutter
Notice that the function returns Future<String>, not a plain String.
Even though we return a String inside the function, Dart wraps the result in a Future.
You can also write:
Future<void> showMessage() async {
print('Welcome to Flutter');
}
The function completes asynchronously with no meaningful return value.
Important: The async keyword alone does not make every operation slow or run it on another thread. It allows a function to use asynchronous operations and return a Future.
What Is Await in Dart?
The await keyword is used inside an async function to wait for a Future to complete before continuing with the next statement.
Let’s understand it with an example.
Future<String> getUserName() async {
await Future.delayed(Duration(seconds: 2));
return 'Rahul';
}
Future<void> main() async {
print('Fetching user...');
String name = await getUserName();
print('User name: $name');
print('Completed');
}
Output:
Fetching user...
User name: Rahul
Completed
Here is what happens:
- The program prints
Fetching user.... - It calls
getUserName(). - The
awaitkeyword waits for the Future to complete. - After two seconds, the function returns
Rahul. - The program prints the username and completion message.
While the function is waiting, Dart can continue processing other events instead of blocking the entire application.
Remember: You can use await inside an async function. It pauses that function’s execution until the awaited Future completes, but it doesn’t block the entire event loop.
How Async and Await Work Together
The async and await keywords are commonly used together.
asyncallows a function to perform asynchronous operations.awaitwaits for a Future result before continuing inside that function.Futurerepresents the result that will be available later.
Let’s look at a complete example:
Future<int> calculateTotal() async {
await Future.delayed(Duration(seconds: 2));
return 100 + 200;
}
Future<void> main() async {
print('Calculation started');
int result = await calculateTotal();
print('Total: $result');
print('Calculation completed');
}
Output:
Calculation started
Total: 300
Calculation completed
This is one of the simplest ways to understand how async and await work together.
Flutter Async and Await with API Calls
One of the most common uses of async and await in Flutter is fetching data from a REST API.
For example, imagine you want to fetch a list of users from a server.
First, add the HTTP package to your Flutter project:
flutter pub add http
Now create a function to fetch users.
Step 1: Create an API Function
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<List<dynamic>> fetchUsers() async {
final url = Uri.parse(
'https://jsonplaceholder.typicode.com/users',
);
final response = await http.get(url);
if (response.statusCode == 200) {
return jsonDecode(response.body) as List<dynamic>;
} else {
throw Exception('Failed to load users');
}
}
Let’s understand the important parts.
1. Create the API URL
final url = Uri.parse(
'https://jsonplaceholder.typicode.com/users',
);
This converts the API URL into a Dart Uri object.
2. Make the API request
final response = await http.get(url);
The http.get() method returns a Future. Using await allows us to wait for the HTTP response before accessing it.
3. Check the response
if (response.statusCode == 200) {
return jsonDecode(response.body) as List<dynamic>;
}
If the server returns HTTP status code 200, we decode the JSON response and return the list of users.
Otherwise, we throw an exception.
For a more detailed API implementation, check our Flutter Dio API Call tutorial.
Step 2: Call the API from a Flutter Screen
Now let’s display the API data inside a Flutter application.
import 'package:flutter/material.dart';
class UsersScreen extends StatefulWidget {
const UsersScreen({super.key});
@override
State<UsersScreen> createState() => _UsersScreenState();
}
class _UsersScreenState extends State<UsersScreen> {
List<dynamic> users = [];
bool isLoading = true;
String? errorMessage;
@override
void initState() {
super.initState();
loadUsers();
}
Future<void> loadUsers() async {
try {
final result = await fetchUsers();
if (!mounted) return;
setState(() {
users = result;
isLoading = false;
});
} catch (error) {
if (!mounted) return;
setState(() {
errorMessage = error.toString();
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Users'),
),
body: isLoading
? const Center(
child: CircularProgressIndicator(),
)
: errorMessage != null
? Center(
child: Text(errorMessage!),
)
: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user['name']),
subtitle: Text(user['email']),
);
},
),
);
}
}
In this example:
initState()calls the API-loading method when the screen is initialized.loadUsers()waits for the API response usingawait.setState()updates the screen after the response arrives.isLoadingcontrols the loading indicator.errorMessagedisplays an error if the request fails.mountedprevents callingsetState()after the widget has been removed from the widget tree.
Notice that initState() itself is not marked async. Instead, it calls a separate asynchronous method. This is the recommended pattern for starting asynchronous work from initState().
Error Handling with Async and Await
API requests and database operations can fail for many reasons.
For example:
- Internet connection problems.
- Server errors.
- Invalid API responses.
- Database failures.
- Unexpected exceptions.
Dart provides try, catch, and finally to handle these situations.
Example:
Future<void> fetchData() async {
try {
print('Fetching data...');
await Future.delayed(Duration(seconds: 2));
print('Data loaded successfully');
} catch (error) {
print('Something went wrong: $error');
} finally {
print('Request completed');
}
}
Future<void> main() async {
await fetchData();
}
Output:
Fetching data...
Data loaded successfully
Request completed
Let’s understand each keyword.
try
The try block contains the code that may produce an error.
catch
The catch block handles an exception if one occurs.
catch (error) {
print('Something went wrong: $error');
}
finally
The finally block executes after the try/catch handling, whether the operation succeeds or fails.
It is useful for cleanup operations, such as resetting a loading state.
Using Async and Await with FutureBuilder
Flutter provides a widget called FutureBuilder for building UI based on the result of a Future.
It automatically rebuilds when the Future completes.
Example:
Future<String> fetchUserName() async {
await Future.delayed(Duration(seconds: 2));
return 'Rahul';
}
Now use this function with FutureBuilder:
import 'package:flutter/material.dart';
class UserScreen extends StatelessWidget {
UserScreen({super.key});
final Future<String> userFuture = fetchUserName();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('FutureBuilder Example'),
),
body: Center(
child: FutureBuilder<String>(
future: userFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text(
'Error: ${snapshot.error}',
);
}
if (snapshot.hasData) {
return Text(
'Welcome ${snapshot.data}',
style: const TextStyle(fontSize: 22),
);
}
return const Text('No data found');
},
),
),
);
}
}
Here, FutureBuilder manages the Future’s connection state and rebuilds the widget when the result changes.
Important: Create the Future outside the build() method or otherwise ensure it is not recreated on every rebuild. In this example, it is stored as a field in the StatelessWidget.
For a more detailed explanation, read our Flutter FutureBuilder tutorial.
Execute Multiple Futures at the Same Time
Sometimes you need to fetch data from multiple APIs or perform multiple asynchronous operations.
You don’t always need to wait for each operation one by one.
Dart provides Future.wait() to wait for multiple Futures to complete.
Example:
Future<String> getUser() async {
await Future.delayed(Duration(seconds: 2));
return 'User data';
}
Future<String> getProducts() async {
await Future.delayed(Duration(seconds: 2));
return 'Product data';
}
Future<void> loadAllData() async {
final results = await Future.wait([
getUser(),
getProducts(),
]);
print(results[0]);
print(results[1]);
}
Future<void> main() async {
await loadAllData();
}
Output:
User data
Product data
Both operations are started when their Futures are passed to Future.wait(). The program waits for the collection of Futures to complete.
This can be useful when loading independent data, such as user details, product lists, and application settings.
Note: If one of the Futures fails, Future.wait() normally completes with an error. Handle failures appropriately based on your application’s requirements.
Difference Between Future and Async/Await
These concepts are related, but they serve different purposes.
| Feature | Future | Async/Await |
|---|---|---|
| Meaning | Represents a result available later | Syntax for writing asynchronous functions |
| Main purpose | Handle asynchronous results | Make asynchronous code easier to read |
| Usage | .then(), .catchError() | async, await, try-catch |
| Readability | Can become nested in complex flows | Usually easier to follow |
| Return type | Future can represent any eventual result | Async functions return a Future |
For example, both approaches can handle the same Future.
Using .then():
getUserName().then((name) {
print(name);
});
Using async and await:
Future<void> displayName() async {
final name = await getUserName();
print(name);
}
Both are valid. Async and await often make sequential asynchronous operations easier to understand.
Common Mistakes When Using Async and Await in Flutter
1. Forgetting the await keyword
If you call an asynchronous function without await, you receive its Future instead of the completed result.
Incorrect:
Future<String> name = getUserName();
print(name);
Correct:
String name = await getUserName();
print(name);
The second example must be inside an async function.
2. Using await inside a non-async function
You cannot use await inside an ordinary function.
Incorrect:
void getData() {
final result = await fetchUsers();
}
Correct:
Future<void> getData() async {
final result = await fetchUsers();
}
3. Making initState async
Flutter’s initState() must remain a synchronous lifecycle method.
Incorrect:
@override
void initState() async {
super.initState();
final data = await fetchUsers();
}
Correct:
@override
void initState() {
super.initState();
loadUsers();
}
Future<void> loadUsers() async {
final data = await fetchUsers();
if (!mounted) return;
setState(() {
users = data;
});
}
4. Calling setState after a widget is disposed
An asynchronous operation may complete after a user has navigated away from the screen.
Calling setState() on a disposed State object can cause an error.
Use the mounted property:
final data = await fetchUsers();
if (!mounted) return;
setState(() {
users = data;
});
5. Not handling exceptions
Network requests and database operations can fail. Always handle errors when your application needs to respond to failures.
try {
final data = await fetchUsers();
} catch (error) {
print('Error: $error');
}
6. Awaiting independent operations one by one
If two operations don’t depend on each other, awaiting them sequentially can increase the total waiting time.
Instead of:
final user = await getUser();
final products = await getProducts();
You can start both operations and wait for them together:
final results = await Future.wait([
getUser(),
getProducts(),
]);
Use this only when the operations are independent and can safely run concurrently.
Best Practices for Async and Await in Flutter
Follow these practices when working with asynchronous operations:
- Use meaningful return types: Prefer
Future<User>orFuture<List<Product>>instead of unnecessary dynamic types. - Handle exceptions: Use
try-catchfor operations that may fail. - Show loading indicators: Provide feedback while users wait for API or database responses.
- Check mounted before setState: Avoid updating disposed widgets after asynchronous operations.
- Avoid unnecessary sequential waits: Use
Future.wait()when independent operations can run together. - Keep UI and business logic separate: Move complex API and database operations into service or repository classes.
- Avoid repeated Future creation in build: Store the Future when using
FutureBuilderso that ordinary rebuilds don’t restart the operation.
Frequently Asked Questions
What is async and await in Flutter?
Async and await are Dart language features used for asynchronous programming in Flutter. The async keyword marks an asynchronous function, while await waits for a Future to complete before continuing inside that function.
What is the difference between async and await in Dart?
async is used to declare an asynchronous function that returns a Future. await is used inside an async function to wait for an asynchronous result.
What is Future in Flutter?
A Future represents a value or error that will become available later. It is commonly used for API requests, database operations, and other asynchronous tasks.
Can we use await without async in Flutter?
In ordinary function bodies, await must be used inside an async function. Dart also supports top-level await in certain contexts, but normal Flutter application code typically uses async functions.
Why do we use async and await in Flutter?
They make asynchronous code easier to read and manage. They are commonly used when fetching API data, accessing databases, reading files, and performing other operations that take time.
Can we use async and await inside initState()?
You should not mark initState() itself as async. Instead, call a separate async function from initState() to perform asynchronous operations.
What is the difference between FutureBuilder and async/await?
Async and await are Dart language features for handling asynchronous operations. FutureBuilder is a Flutter widget that builds its UI based on the state of a Future.
Is async and await the same as multithreading?
No. Async and await do not automatically create a new thread. Dart uses its event loop to manage asynchronous operations. CPU-intensive work may require an isolate to avoid blocking the main isolate.
Conclusion
Flutter async and await are essential concepts for developing responsive mobile applications. They make it easier to work with APIs, databases, Firebase, file operations, and other tasks that don’t complete immediately.
In this tutorial, you learned about synchronous and asynchronous programming, Future, async and await, API requests, error handling, FutureBuilder, and executing multiple Futures together.
Start by practising simple asynchronous functions, then use these concepts in real Flutter applications. Once you understand them, working with API integrations and database operations becomes much easier.
For more learning, explore the official Dart Asynchronous Programming documentation and Flutter documentation.
