In the previous lesson, we learned the basics of asynchronous programming - Future and async/await, mastering how to write asynchronous code with a synchronous style. But in real-world development, exception handling for asynchronous operations, coordination of multiple asynchronous tasks, and performance optimization are equally important. Today we'll dive into advanced asynchronous content to solve these more complex problems.
I. Asynchronous Exception Handling: In-depth Practice with try-catch + async
Exception handling in asynchronous operations is more complex than in synchronous code because errors may be thrown "at some point in the future". Combining try-catch with async/await is the best practice for handling asynchronous exceptions, but there are some details to note.
1. Basic Catching Method
In an async function, if code after await throws an exception, it will be caught by try-catch, just like in synchronous code:
Future<void> riskyOperation() async {
await Future.delayed(Duration(seconds: 1));
// Simulate throwing an exception in an asynchronous operation
throw Exception("Asynchronous operation failed: network timeout");
}
void main() async {
try {
print("Starting risky operation");
await riskyOperation(); // Wait for asynchronous operation; exceptions will be caught
print("Operation completed successfully completed (won't execute)");
} catch (e) {
// Catch asynchronous exception
print("Caught exception: ${e.toString()}");
} finally {
// Executes regardless of success or failure
print("Operation finished, cleaning up resources");
}
}
// Output:
// Starting risky operation
// Caught exception: Exception: Asynchronous operation failed: network timeout
// Operation finished, cleaning up resources
2. Exception Handling in Nested Asynchronous Calls
When an async function contains multiple asynchronous calls, try-catch can catch all nested exceptions:
Future<void> level3() async {
await Future.delayed(Duration(milliseconds: 500));
throw Exception("Innermost error"); // Throw exception
}
Future<void> level2() async {
await level3(); // Call inner asynchronous function
}
Future<void> level1() async {
await level2(); // Call middle asynchronous function
}
void main() async {
try {
await level1(); // Call outer asynchronous function
} catch (e) {
// Exceptions from all levels will be caught
print(
"Top level caught exception: $e",
); // Output: Top level caught exception: Exception: Innermost error
}
}
This demonstrates the advantage of async/await: exceptions propagate like they do in synchronous code, avoiding nested exception handling in callback hell.
3. Differentiating Between Exception Types
You can catch specific types of exceptions with on Type catch for granular handling:
// Custom exception types
class NetworkException implements Exception {
final String message;
NetworkException(this.message);
@override
String toString() => "NetworkException: $message";
}
class DatabaseException implements Exception {
final String message;
DatabaseException(this.message);
@override
String toString() => "DatabaseException: $message";
}
Future<void> fetchData() async {
// Simulate random exceptions
await Future.delayed(Duration(seconds: 1));
if (DateTime.now().second % 2 == 0) {
throw NetworkException("Server connection failed");
} else {
throw DatabaseException("Data query error");
}
}
void main() async {
try {
await fetchData();
} on NetworkException catch (e) {
// Only catch NetworkException
print("Network error handling: $e");
// Retry logic could go here
} on DatabaseException catch (e) {
// Only catch DatabaseException
print("Database error handling: $e");
// Data repair logic could go here
} catch (e) {
// Catch all other exceptions
print("Unknown error: $e");
}
}
4. Dangers of Uncaught Exceptions
If an asynchronous exception isn't caught, it will crash the program (in Flutter, this causes app crashes):
Future<void> unhandledError() async {
throw Exception("Uncaught exception");
}
void main() {
unhandledError(); // Call asynchronous function but don't handle exceptions
print("Program continues executing...");
// Output:
// Program continues executing...
// (Later) Unhandled exception: Exception: Uncaught exception
}
Best Practice: All await calls must be inside try-catch blocks or have error handling via .catchError.
II. Future Combinations: Coordinating Multiple Asynchronous Tasks
Real-world development often requires handling multiple asynchronous tasks. Dart provides utility methods like Future.wait and Future.any to help us flexibly combine multiple Futures.
1. Future.wait: Waiting for All Tasks to Complete (Successfully)
When you need all asynchronous tasks to complete before proceeding (like loading multiple resources in parallel), use Future.wait:
// Simulate three different asynchronous tasks
Future<String> fetchUser() =>
Future.delayed(Duration(seconds: 1), () => "User data");
Future<String> fetchOrders() =>
Future.delayed(Duration(seconds: 2), () => "Order data");
Future<String> fetchProducts() =>
Future.delayed(Duration(seconds: 1), () => "Product data");
void main() async {
print("Starting parallel data loading...");
// Wait for all Futures to complete (total time depends on slowest task, 2 seconds here)
List<String> results = await Future.wait([
fetchUser(),
fetchOrders(),
fetchProducts(),
]);
// Executes only after all tasks complete successfully
print("All data loaded:");
print("User: ${results[0]}");
print("Orders: ${results[1]}");
print("Products: ${results[2]}");
}
// Output:
// Starting parallel data loading...
// (2-second wait)
// All data loaded:
// User: User data
// Orders: Order data
// Products: Product data
Note: If any task throws an exception in Future.wait, the entire wait immediately fails and throws that exception:
Future<String> fetchSuccess() => Future.value("Success data");
Future<String> fetchFailure() => Future.delayed(Duration(seconds: 1), () {
throw Exception("One task failed");
});
void main() async {
try {
await Future.wait([fetchSuccess(), fetchFailure()]);
} catch (e) {
print(
"Caught exception: $e",
); // Output: Caught exception: Exception: One task failed
}
}
If you need to continue waiting for other tasks even if some fail, add error handling to each individual Future:
Future<String> fetchSuccess() => Future.value("Success data");
Future<String> fetchFailure() => Future.delayed(Duration(seconds: 1), () {
throw Exception("One task failed");
});
void main() async {
// Add catchError to each Future to convert errors to normal values
List<Future<String>> futures = [
fetchSuccess(),
fetchFailure().catchError((e) => "Error data: ${e.toString()}"),
];
List<String> results = await Future.wait(futures);
print(
results,
); // Output: [Success data, Error data: Exception: One task failed]
}
2. Future.any: Waiting for the First Completed Task
When you need only one task to complete among multiple (like downloading from multiple mirror servers and choosing the fastest), use Future.any:
// Simulate tasks with different speeds
Future<String> fastTask() =>
Future.delayed(Duration(seconds: 1), () => "Fast task result");
Future<String> mediumTask() =>
Future.delayed(Duration(seconds: 2), () => "Medium task result");
Future<String> slowTask() =>
Future.delayed(Duration(seconds: 3), () => "Slow task result");
void main() async {
print("Waiting for first completed task...");
// Wait only for the first completed task (fastTask completes in 1 second here)
String firstResult = await Future.any([fastTask(), mediumTask(), slowTask()]);
print(
"First completed task result: $firstResult",
); // Output: First completed task result: Fast task result
}
Note: The first failing task in a Future.any will cause the entire task to fail, unless that task has been individually handled for errors:
Future<String> fastFailure() => Future.delayed(Duration(seconds: 1), () {
throw Exception("Fast failure");
});
Future<String> slowSuccess() =>
Future.delayed(Duration(seconds: 2), () => "Slow success");
void main() async {
try {
// The first one to complete is fastFailure, which will cause the entire process to fail.
await Future.any([fastFailure(), slowSuccess()]);
} catch (e) {
print("Caught exception:$e"); // Output: Caught exception: Exception: Fail fast
}
// Correct handling: Add error handling for tasks that may fail first
String result = await Future.any([
fastFailure().catchError((e) => "error"), // Returns null on error
slowSuccess(),
]);
print("Final result: $result"); // Output: Final result: error
}
3. Future.forEach: Processing Iterables Sequentially
Future.forEach processes asynchronous operations in sequence (one completes before the next starts), ideal for scenarios requiring serial processing:
void main() async {
List<int> ids = [1, 2, 3];
// Process each id sequentially, waiting for one to complete before next
await Future.forEach(ids, (int id) async {
await Future.delayed(
Duration(seconds: 1),
); // Simulate time-consuming operation
print("Processed id: $id");
});
print("All ids processed");
}
// Output (one line every second):
// Processed id: 1
// Processed id: 2
// Processed id: 3
// All ids processed
III. Asynchronous Loops and Performance Pitfalls: Avoid Creating Futures in Loops
When handling asynchronous operations in loops, it's easy to fall into performance traps. Incorrect implementations can cause resource waste or inefficiency.
1. Bad Practice: Creating Many Parallel Futures in a Loop
// Simulate a single asynchronous task
Future<void> processItem(int i) async {
await Future.delayed(Duration(milliseconds: 100));
// print("Processed item $i");
}
void main() async {
final stopwatch = Stopwatch()..start();
// Bad: Creates 1000 parallel Futures at once
List<Future> futures = [];
for (int i = 0; i < 1000; i++) {
futures.add(processItem(i));
}
await Future.wait(futures);
stopwatch.stop();
print(
"Total time: ${stopwatch.elapsedMilliseconds} ms",
); // ~100-200 ms (parallel advantage)
}
While parallel processing seems fast, with very large loop counts (like 100,000):
- It creates many Future objects instantly, consuming significant memory
- May hit system limits on concurrent connections (for network requests)
- Causes CPU or I/O resource contention, actually reducing efficiency
2. Improved Practice: Controlling Concurrency
Avoid resource exhaustion by processing in batches or limiting concurrency:
Future<void> processItem(int i) async {
await Future.delayed(Duration(milliseconds: 100));
// print("Processed item $i");
}
// Batch processing: process 10 items at a time, wait for batch completion before next
Future<void> processInBatches(List<int> items, int batchSize) async {
for (int i = 0; i < items.length; i += batchSize) {
int end = i + batchSize;
List<int> batch = items.sublist(i, end > items.length ? items.length : end);
// Process current batch in parallel
await Future.wait(batch.map((item) => processItem(item)));
print("Completed batch ${i ~/ batchSize + 1}");
}
}
void main() async {
final stopwatch = Stopwatch()..start();
// Create 1000 items to process
List<int> items = List.generate(1000, (index) => index);
// Process 10 items per batch
await processInBatches(items, 10);
stopwatch.stop();
print(
"Total time: ${stopwatch.elapsedMilliseconds} ms",
); // ~1000 ms (10 batches × 100 ms)
}
While total time increases, this avoids resource contention and system limits, making it suitable for large-scale processing.
3. Correct Sequential Loop: Using await for Control
If tasks must execute sequentially (each depends on the previous result), use await inside the loop:
Future<void> processItem(int i) async {
await Future.delayed(Duration(milliseconds: 100));
// print("Processed item $i");
}
void main() async {
final stopwatch = Stopwatch()..start();
// Correct: Execute sequentially, one completes before next starts
for (int i = 0; i < 5; i++) {
await processItem(i); // Wait for current task to complete in each iteration
}
stopwatch.stop();
print(
"Total time: ${stopwatch.elapsedMilliseconds} ms",
); // ~500 ms (5 × 100 ms)
}
Note: Don't collect Futures outside the loop then wait - this creates parallel execution:
Future<void> processItem(int i) async {
await Future.delayed(Duration(milliseconds: 100));
// print("Processed item $i");
}
// Wrong: Looks sequential but executes in parallel
void main() async {
List<Future> futures = [];
for (int i = 0; i < 5; i++) {
futures.add(processItem(i)); // Creates all Futures immediately
}
await Future.wait(futures); // Executes in parallel, total time ~100 ms
}
IV. Debugging Techniques for Asynchronous Code
Debugging asynchronous code is more complex than synchronous code because execution order isn't straightforward. Here are useful techniques:
- Use print or logging tools: Print timestamps and variable states at key points to track execution order.
- Leverage async function stack traces: Dart records call stacks for asynchronous operations, showing complete paths when exceptions occur:
Future<void> a() async => await b();
Future<void> b() async => await c();
Future<void> c() async => throw Exception("Error occurred");
void main() async {
try {
await a();
} catch (e, stackTrace) {
print("Exception: $e");
print(
"Stack trace: $stackTrace",
); // Shows complete path from c → b → a → main
}
}
3 . Debug with Flutter DevTools: Flutter's DevTools includes a dedicated asynchronous task tracking panel to visually inspect all active Futures.
Top comments (0)