Dart Asynchronous Patterns
In this lesson, you will learn asynchronous design patterns in Dart. These patterns help you write clean, scalable, and maintainable async code.
As applications grow, simple async/await is not always enough.
Asynchronous patterns provide structure for handling complex workflows.
Why Asynchronous Patterns Matter
Without proper patterns, async code can become:
- Difficult to read
- Hard to debug
- Error-prone
- Unscalable
Patterns solve these issues by defining clear execution flows.
Pattern 1: Sequential Async Execution
Tasks are executed one after another. Each task waits for the previous one to finish.
This pattern is useful when tasks depend on earlier results.
Future fetchUserId() async {
await Future.delayed(Duration(seconds: 1));
return 101;
}
Future fetchUserName(int id) async {
await Future.delayed(Duration(seconds: 1));
return "User_$id";
}
void main() async {
int id = await fetchUserId();
String name = await fetchUserName(id);
print(name);
}
Use this when execution order matters.
Pattern 2: Parallel Execution
Multiple independent tasks can run concurrently. This improves performance.
Future fetchOrders() async {
await Future.delayed(Duration(seconds: 2));
return 12;
}
Future fetchRevenue() async {
await Future.delayed(Duration(seconds: 2));
return 12500.50;
}
void main() async {
var results = await Future.wait([
fetchOrders(),
fetchRevenue()
]);
print("Orders: ${results[0]}");
print("Revenue: ${results[1]}");
}
This pattern reduces overall execution time.
Pattern 3: Fire-and-Forget
Some tasks do not require waiting for completion. They can be triggered and ignored safely.
void logAnalytics() {
Future(() {
print("Analytics logged");
});
}
void main() {
print("App Started");
logAnalytics();
print("Continue App Flow");
}
Use this for logging, metrics, and background notifications.
Pattern 4: Retry Pattern
Network calls often fail. Retry logic improves reliability.
Future fetchData() async {
await Future.delayed(Duration(seconds: 1));
throw Exception("Network error");
}
Future retryFetch(int retries) async {
for (int i = 0; i < retries; i++) {
try {
return await fetchData();
} catch (e) {
print("Retry attempt ${i + 1}");
}
}
return "Failed after retries";
}
void main() async {
String result = await retryFetch(3);
print(result);
}
This pattern is common in API-based systems.
Pattern 5: Timeout Pattern
Timeouts prevent waiting forever on slow operations.
Future slowTask() async {
await Future.delayed(Duration(seconds: 5));
return "Completed";
}
void main() async {
try {
String result = await slowTask()
.timeout(Duration(seconds: 2));
print(result);
} catch (e) {
print("Operation timed out");
}
}
Timeouts improve responsiveness and fault tolerance.
Pattern 6: Stream-Based Pattern
Streams are ideal for handling continuous or real-time data.
Stream numberStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() async {
await for (var value in numberStream()) {
print(value);
}
}
Used for sensors, sockets, and live updates.
Pattern 7: Chained Futures
Chaining allows transforming results step by step.
Future getBaseValue() async {
return 10;
}
void main() {
getBaseValue()
.then((value) => value * 2)
.then((value) => value + 5)
.then((result) => print(result));
}
Chaining is useful for simple pipelines.
Choosing the Right Pattern
- Sequential – dependent steps
- Parallel – independent tasks
- Fire-and-forget – background work
- Retry / Timeout – network reliability
- Streams – continuous data
📝 Practice Exercises
Exercise 1
Create two async functions and execute them in parallel.
Exercise 2
Add timeout handling to a delayed Future.
Exercise 3
Build a stream that emits random numbers every second.
What’s Next?
In the next lesson, you will apply concurrency and async patterns to real-world Dart scenarios, including performance and scalability examples.