Dart Async Programming — Futures, Async, and Await
In this tutorial, you will learn about Dart Async Programming. We cover key concepts, practical examples, and best practices to help you master this topic.
Dart async programming uses Future objects and async/await syntax to perform non-blocking operations, allowing applications to remain responsive while waiting for I/O, network requests, or timers.
What You Will Learn
- Understanding asynchronous programming and the event loop
- Creating and using Future objects
- The async and await keywords
- Error handling in async code
- Running operations concurrently with Future.wait
- Delaying execution with Future.delayed
- Converting callbacks to async/await
Why It Matters
Most real-world applications need to perform operations that take time: HTTP requests, database queries, file I/O, or timers. Without async programming, these operations block the entire application, causing UI freezes in Flutter and unresponsive servers. Dart's async model is based on a single-threaded event loop with futures and streams. Understanding this model is critical because Flutter uses it for every user interaction, animation, and network call.
Real-World Use
The DodaTech Flutter app uses async/await for every network request. When a user searches for a course, the app awaits the API response while showing a loading indicator. The UI remains responsive during the wait. All database operations with SQLite use async methods, and file downloads show progress using Streams.
Learning Path
flowchart LR A[Dart Generics] --> B[Async Programming\nYou are here] B --> C[Dart Streams] style B fill:#f90,color:#fff
The Event Loop
Dart runs on a single thread with an event loop. Synchronous code runs immediately. Asynchronous operations are scheduled to run later:
void main() {
print('Start');
Future(() => print('Future 1'));
Future(() => print('Future 2'));
print('End');
}
Output:
Start
End
Future 1
Future 2
Even though Future 1 and Future 2 are created before End is printed, they execute after the current synchronous block completes. The event loop processes them when the main code finishes.
Creating a Future
A Future represents a value that will be available at some point in the future:
void main() {
print('Before future');
var future = Future<String>(() {
print('Inside future (async)');
return 'Hello from future';
});
future.then((value) {
print('Future completed with: $value');
});
print('After future');
}
Output:
Before future
After future
Inside future (async)
Future completed with: Hello from future
The callback passed to Future() runs asynchronously. The .then() method registers a callback that runs when the future completes. The main code continues without waiting.
Async and Await
The async and await keywords make asynchronous code look synchronous:
Future<String> fetchUserData() {
return Future.delayed(Duration(seconds: 2), () {
return 'User: Alice';
});
}
Future<void> main() async {
print('Fetching user data...');
var data = await fetchUserData();
print('Data received: $data');
print('Done');
}
Output:
Fetching user data...
(2 second pause)
Data received: User: Alice
Done
The await keyword pauses the execution of the main function until the future completes. The function must be marked async. While main is awaiting, the event loop can Process other events, so the application remains responsive.
Error Handling
Errors in async functions are caught with try-catch:
Future<String> fetchFromApi(bool shouldFail) {
return Future.delayed(Duration(seconds: 1), () {
if (shouldFail) {
throw Exception('Network error');
}
return 'Success: data loaded';
});
}
Future<void> main() async {
try {
var result = await fetchFromApi(true);
print(result);
} catch (e) {
print('Error caught: $e');
}
// Using .catchError()
await fetchFromApi(true)
.then((value) => print(value))
.catchError((error) => print('Caught: $error'));
print('Program continues');
}
Output:
Error caught: Exception: Network error
Caught: Exception: Network error
Program continues
Use try-catch with async/await for familiar error handling. The .catchError() method is used with the .then() chain style. Unhandled errors in futures cause unhandled exception events.
Async/Await with Multiple Operations
Await multiple futures sequentially or concurrently:
Future<String> fetchUser(int id) async {
await Future.delayed(Duration(seconds: 1));
return 'User $id';
}
Future<String> fetchOrders(String user) async {
await Future.delayed(Duration(seconds: 1));
return 'Orders for $user';
}
Future<void> main() async {
// Sequential: total 2 seconds
print('Sequential execution:');
var start = DateTime.now();
var user = await fetchUser(1);
var orders = await fetchOrders(user);
print('${DateTime.now().difference(start).inSeconds}s: $orders');
// Concurrent: total 1 second
print('\nConcurrent execution:');
start = DateTime.now();
var results = await Future.wait([
fetchUser(1),
fetchOrders('User 1'),
]);
print('${DateTime.now().difference(start).inSeconds}s: $results');
}
Output:
Sequential execution:
(1 second pause)
(1 second pause)
2s: Orders for User 1
Concurrent execution:
(1 second pause)
1s: [User 1, Orders for User 1]
Use Future.wait() when operations are independent and can execute in parallel. Sequential awaiting is appropriate when later operations depend on the results of earlier ones.
Future.delayed and Future.value
Dart provides utility constructors for common Future patterns:
void main() async {
// Future.delayed: wait before completing
print('Waiting 1 second...');
await Future.delayed(Duration(seconds: 1));
print('Done waiting');
// Future.value: immediately complete with a value
var immediate = Future.value(42);
print('Immediate: ${await immediate}');
// Future.error: immediately complete with an error
var error = Future.error('Something went wrong');
try {
await error;
} catch (e) {
print('Error result: $e');
}
// Future.microtask: schedule on microtask queue
Future.microtask(() => print('Microtask runs'));
print('This prints first');
}
Output:
Waiting 1 second...
(1 second pause)
Done waiting
Immediate: 42
Error result: Something went wrong
This prints first
Microtask runs
Future.microtask schedules a task on the microtask queue, which runs before the event queue. Use it for short operations that should complete before the next frame.
Converting Callbacks to Futures
Older APIs use callbacks. Convert them to Futures for cleaner code:
import 'dart:io';
// Simulating a callback-based API
class LegacyApi {
void fetchData(void Function(String data) onSuccess, void Function(String error) onError) {
// Simulate work
Future.delayed(Duration(seconds: 1), () {
onSuccess('Legacy data loaded');
});
}
}
// Convert to Future-based API
Future<String> fetchDataAsync() {
var api = LegacyApi();
var completer = Completer<String>();
api.fetchData(
(data) => completer.complete(data),
(error) => completer.completeError(error),
);
return completer.future;
}
Future<void> main() async {
print('Fetching from legacy API...');
try {
var data = await fetchDataAsync();
print('Got: $data');
} catch (e) {
print('Error: $e');
}
}
Output:
Fetching from legacy API...
(1 second pause)
Got: Legacy data loaded
Completer<T> creates a Future that you can complete manually. Call completer.complete(value) on success or completer.completeError(error) on failure.
Timeouts
Use .timeout() to set a maximum wait time for a Future:
Future<String> slowOperation() async {
await Future.delayed(Duration(seconds: 5));
return 'Slow result';
}
Future<void> main() async {
print('Starting operation...');
try {
var result = await slowOperation().timeout(
Duration(seconds: 3),
onTimeout: () => 'Timeout fallback',
);
print('Result: $result');
} catch (e) {
print('Operation failed: $e');
}
}
Output:
Starting operation...
(3 second pause)
Result: Timeout fallback
Without onTimeout, a timeout throws TimeoutException. With onTimeout, the provided value or Future is used instead. This is useful for providing degraded experiences when services are slow.
Async Streams
Streams are the async version of Iterables. They provide a sequence of values over time:
import 'dart:async';
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(Duration(milliseconds: 500));
yield i;
}
}
Future<void> main() async {
print('Counting...');
await for (var value in countStream(5)) {
print('Count: $value');
}
print('Done counting');
}
Output:
Counting...
Count: 1
(500ms pause)
Count: 2
(500ms pause)
Count: 3
(500ms pause)
Count: 4
(500ms pause)
Count: 5
Done counting
The async* keyword marks a function as an asynchronous generator. yield emits values one at a time. The await for loop consumes the stream, processing each value as it arrives.
Common Mistakes
Forgetting to
awaita Future: Withoutawait, the function continues immediately and returns a Future instead of the value. The compiler warns about unused futures, but it is easy to miss.Blocking the event loop with synchronous operations:
Future.delayedandawaitdo not block. But callingsleep()or doing CPU-intensive work in aFuturecallback blocks the event loop. UseIsolatefor CPU-heavy tasks.Not handling errors in async functions: An unhandled error in an async function creates an unhandled Future that may crash the application. Always wrap async code in try-catch.
Using
.then()whenawaitis clearer:.then()chains can create deeply nested code. Preferawaitfor readability, but.then()can be useful for simple transformations.Creating
Futureconstructors that execute immediately: The callback passed toFuture()runs asynchronously, not immediately. If you need immediate execution, call the function directly without wrapping inFuture.
Practice Questions
- How does the Dart event loop process synchronous and asynchronous code?
- What is the difference between
Future.waitand awaiting futures sequentially? - How does
Completerhelp bridge callback-based APIs to Future-based APIs? - What happens when a Future times out?
- Challenge: Write a function that retries an asynchronous operation up to 3 times with exponential backoff (500ms, 1s, 2s delays). The function should return the result on success or throw after all retries fail.
Mini Project
Build an asynchronous data loading system:
- Implement
fetchUser(int id)that simulates a 1-second API call - Implement
fetchPosts(int userId)that simulates a 1.5-second API call - Implement
fetchComments(int postId)that simulates a 0.5-second API call - Load user, posts, and comments concurrently using
Future.wait - Add timeout handling for slow responses
- Add retry logic for failed requests
- Measure and print execution time
FAQ
What is Next
Now that you understand async programming, learn about streams for continuous data. Proceed to Dart Streams for stream controllers, transformations, and subscriptions. Then explore Dart Isolates for parallel execution.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro