async & await
Understand how to write asynchronous code that looks synchronous using async and await in Dart.
What is it?
async and await are keywords that make working with Futures easier and more readable. async marks a function as asynchronous, allowing it to use await. await pauses the execution of the function until a Future completes, without blocking the main thread.
Why does it exist?
async & await exist to:
- Write asynchronous code that looks synchronous
- Eliminate callback nesting ("callback hell")
- Make code more readable and maintainable
- Simplify error handling with try-catch
- Reduce boilerplate code
- Make async code easier to reason about
Basic async/await
Using async and await
The
asynckeyword marks a function as asynchronous. Inside anasyncfunction, you can useawaitto wait for Futures to complete. The function automatically returns aFutureof the return type.
// Function that returns a Future<String>
Future<String> fetchData() async {
// Simulate network delay
await Future.delayed(Duration(seconds: 2));
return 'Data loaded';
}
// Using the async function
void main() async {
print('Loading...');
// Wait for the Future to complete
String result = await fetchData();
print('Result: $result');
print('Done!');
}
// Output:
// Loading...
// (2 seconds later)
// Result: Data loaded
// Done!
What's happening here? -
asyncmarksfetchData()as asynchronous -awaitpauses execution untilFuture.delayed()completes - The function returns aFuture<String>automatically - Inmain(),await fetchData()waits for the result - The program looks sequential but runs asynchronously
async Function Return Types
When you mark a function with
async, Dart automatically wraps the return value in aFuture. The return type must be aFutureof the actual return type.
// Returns Future<String>
Future<String> getGreeting() async {
await Future.delayed(Duration(seconds: 1));
return 'Hello, World!';
}
// Returns Future<int>
Future<int> getNumber() async {
await Future.delayed(Duration(seconds: 1));
return 42;
}
// Returns Future<void> (no return value)
Future<void> doSomething() async {
await Future.delayed(Duration(seconds: 1));
print('Something done!');
}
// Returns Future<List<String>>
Future<List<String>> getNames() async {
await Future.delayed(Duration(seconds: 1));
return ['Alice', 'Bob', 'Charlie'];
}
// Usage
void main() async {
var greeting = await getGreeting();
var number = await getNumber();
await doSomething();
var names = await getNames();
print(greeting); // Hello, World!
print(number); // 42
print(names); // [Alice, Bob, Charlie]
}
Key insights: -
asyncautomatically wraps return inFuture- Return type must match what's actually returned -Future<void>is for functions with no return value - The return type can be any type wrapped in Future
Error Handling with async/await
Try-Catch with async/await
Error handling with
async/awaituses the sametry-catchsyntax as synchronous code. This makes error handling much more natural.
// Function that throws an error
Future<String> riskyOperation() async {
await Future.delayed(Duration(seconds: 1));
throw Exception('Operation failed!');
}
// Function that might succeed or fail
Future<int> fetchNumber(bool shouldFail) async {
await Future.delayed(Duration(seconds: 1));
if (shouldFail) {
throw FormatException('Invalid format');
}
return 42;
}
void main() async {
// Standard try-catch
try {
var result = await riskyOperation();
print('Success: $result');
} catch (e) {
print('Error: $e');
}
// Try-catch with specific error types
try {
var number = await fetchNumber(true);
print('Number: $number');
} on FormatException catch (e) {
print('Format error: $e');
} catch (e) {
print('Other error: $e');
} finally {
print('Always executes');
}
}
// Output:
// Error: Exception: Operation failed!
// Format error: Invalid format
// Always executes
What's happening here? -
try-catchworks naturally withawait-oncan catch specific exception types -finallyalways executes, success or failure - Error handling is clean and readable
Multiple Async Operations
You can perform multiple asynchronous operations sequentially or in parallel using
async/await.
// Sequential operations
Future<String> fetchUser() async {
await Future.delayed(Duration(seconds: 1));
return 'User: Alice';
}
Future<String> fetchProfile(String user) async {
await Future.delayed(Duration(seconds: 1));
return '$user - Profile: Active';
}
Future<String> fetchPosts(String profile) async {
await Future.delayed(Duration(seconds: 1));
return '$profile - Posts: 5';
}
// Sequential execution
Future<void> sequentialExample() async {
print('Sequential:');
var user = await fetchUser();
print(user);
var profile = await fetchProfile(user);
print(profile);
var posts = await fetchPosts(profile);
print(posts);
}
// Parallel execution with Future.wait
Future<void> parallelExample() async {
print('Parallel:');
var results = await Future.wait([
fetchUser(),
fetchProfile('User: Alice'),
fetchPosts('User: Alice - Profile: Active'),
]);
print(results);
}
// Parallel with individual await
Future<void> parallelAwaitExample() async {
print('Parallel with individual awaits:');
var future1 = fetchUser();
var future2 = fetchProfile('User: Alice');
var future3 = fetchPosts('User: Alice - Profile: Active');
var user = await future1;
var profile = await future2;
var posts = await future3;
print('$user, $profile, $posts');
}
What's happening here? - Sequential operations run one after another - Parallel operations run simultaneously -
Future.wait()runs multiple Futures in parallel - Individualawaiton pre-started Futures - Choose based on your needs
Advanced Patterns
Conditional Async Operations
You can use
async/awaitwith conditional logic, loops, and other control flow structures.
// Conditional async operations
Future<String> fetchData(bool fromCache) async {
if (fromCache) {
// Simulate cache hit
await Future.delayed(Duration(milliseconds: 100));
return 'Cached data';
} else {
// Simulate network request
await Future.delayed(Duration(seconds: 2));
return 'Fresh data';
}
}
// Looping with async
Future<List<String>> fetchAllUsers(List<int> ids) async {
List<String> users = [];
for (var id in ids) {
await Future.delayed(Duration(milliseconds: 500));
users.add('User $id');
}
return users;
}
// Retry logic
Future<String> fetchWithRetry(int maxRetries) async {
for (var attempt = 1; attempt <= maxRetries; attempt++) {
try {
await Future.delayed(Duration(seconds: 1));
if (attempt < 3) {
throw Exception('Temporary failure');
}
return 'Success on attempt $attempt';
} catch (e) {
print('Attempt $attempt failed: $e');
if (attempt == maxRetries) {
rethrow;
}
}
}
return 'Unreachable';
}
void main() async {
// Conditional
var data = await fetchData(false);
print(data); // Fresh data
// Loop
var users = await fetchAllUsers([1, 2, 3]);
print(users); // [User 1, User 2, User 3]
// Retry
try {
var result = await fetchWithRetry(3);
print(result); // Success on attempt 3
} catch (e) {
print('All retries failed');
}
}
Key insights: -
async/awaitworks with if/else, loops, etc. - Retry logic is straightforward with loops - Conditional async operations are natural - Complex flows are easier with async/await
Streams with async/await
While
async/awaitis primarily for Futures, you can use it with Streams usingawait for.
// Stream that emits numbers
Stream<int> countStream(int max) async* {
for (var i = 1; i <= max; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
// Processing Stream with await for
Future<void> processStream() async {
print('Processing stream:');
await for (var value in countStream(5)) {
print('Received: $value');
}
print('Stream complete!');
}
// Combining Future and Stream
Future<int> sumStream(Stream<int> stream) async {
int sum = 0;
await for (var value in stream) {
sum += value;
}
return sum;
}
void main() async {
await processStream();
var total = await sumStream(countStream(5));
print('Sum: $total'); // 15
}
// Output:
// Processing stream:
// (1 second later)
// Received: 1
// (1 second later)
// Received: 2
// (1 second later)
// Received: 3
// (1 second later)
// Received: 4
// (1 second later)
// Received: 5
// Stream complete!
// Sum: 15
What's happening here? -
await forloops over Stream events - Each event pauses the function until received - Streams can be processed sequentially - Combine with Futures for complex logic
Best Practices
Use async/await for Readability
// Bad: Callback hell with raw Futures
void badExample() {
fetchUser()
.then((user) {
fetchProfile(user)
.then((profile) {
fetchPosts(profile)
.then((posts) {
print('Posts: $posts');
})
.catchError((error) {
print('Error: $error');
});
})
.catchError((error) {
print('Error: $error');
});
})
.catchError((error) {
print('Error: $error');
});
}
// Good: Clean async/await
void goodExample() async {
try {
var user = await fetchUser();
var profile = await fetchProfile(user);
var posts = await fetchPosts(profile);
print('Posts: $posts');
} catch (e) {
print('Error: $e');
}
}
Don't Use await in Parallel
// Bad: Sequential when it could be parallel
void badExample() async {
var user = await fetchUser();
var profile = await fetchProfile(); // Could be parallel
var posts = await fetchPosts(); // Could be parallel
}
// Good: Parallel when possible
void goodExample() async {
// Start all Futures at once
var userFuture = fetchUser();
var profileFuture = fetchProfile();
var postsFuture = fetchPosts();
// Then await all results
var user = await userFuture;
var profile = await profileFuture;
var posts = await postsFuture;
}
// Even better: Future.wait
void bestExample() async {
var results = await Future.wait([
fetchUser(),
fetchProfile(),
fetchPosts(),
]);
var user = results[0];
var profile = results[1];
var posts = results[2];
}
Common Mistakes
Forgetting to Mark Function async
Wrong:
void main() {
var data = await fetchData(); // Error: await only in async
print(data);
}
Correct:
void main() async {
var data = await fetchData();
print(data);
}
Returning Future in async Function
Wrong:
Future<String> fetchData() async {
return Future.value('Data'); // Wrapping extra Future
}
Correct:
Future<String> fetchData() async {
return 'Data'; // Automatically wrapped in Future
}
Not Using try-catch in Async
Wrong:
void main() async {
var result = await riskyOperation(); // Unhandled error
print(result);
}
Correct:
void main() async {
try {
var result = await riskyOperation();
print(result);
} catch (e) {
print('Error: $e');
}
}
Summary
async and await make asynchronous code look and behave like synchronous code. They eliminate callback nesting, simplify error handling, and make code more readable and maintainable.
Next Steps
Now that you understand async & await, continue to:
Did You Know?
asyncfunctions always return a Futureawaitcan only be used insideasyncfunctions- The event loop processes microtasks before normal tasks
async/awaitis syntactic sugar over Future- You can use
try-catchwithawaitnaturally Future.wait()runs operations in parallel- The Dart VM optimizes async/await for performance