Introduction
In Dart programming, asynchronous operations are often handled using Futures. Future.whenComplete is a method that allows you to register a callback that will be called when the future completes, whether it completes with a value or with an error. This callback will be executed regardless of the future's outcome, making it useful for cleanup operations or logging.
History/Background
Introduced in Dart 2.1, Future.whenComplete was added to provide developers with a way to execute code after a future completes, regardless of whether the future was successful or resulted in an error. This feature enhances the flexibility and robustness of handling asynchronous operations in Dart.
Syntax
Future<T> whenComplete(void action())
-
T: The type of the future's value. -
action: The callback function to be executed when the future completes. - Executes a callback function after the future completes, regardless of its outcome.
- Useful for performing cleanup tasks, logging, or any actions that need to happen regardless of the future's result.
- Returns a new future that completes with the same result as the original future.
Key Features
Example 1: Basic Usage
void main() {
Future<int>.delayed(Duration(seconds: 2), () => 42)
.then((value) {
print('Future completed with value: $value');
})
.whenComplete(() {
print('Future operation completed.');
});
}
Output:
Future completed with value: 42
Future operation completed.
Example 2: Error Handling
void main() {
Future<int>.delayed(Duration(seconds: 2), () => throw Exception('Error occurred'))
.then((value) {
print('Future completed with value: $value');
})
.catchError((error) {
print('Error: $error');
})
.whenComplete(() {
print('Future operation completed.');
});
}
Output:
Error: Exception: Error occurred
Future operation completed.
Example 3: Chaining with Future.whenComplete
void main() {
Future<int>.delayed(Duration(seconds: 2), () => 42)
.then((value) {
print('Future completed with value: $value');
})
.whenComplete(() {
print('Future operation completed.');
})
.then((_) => print('Chained operation after whenComplete.'));
}
Output:
Future completed with value: 42
Future operation completed.
Chained operation after whenComplete.
Common Mistakes to Avoid
1. Ignoring the Return Value of `whenComplete`
Problem: Beginners often overlook that whenComplete returns a new Future, which can be used for further chaining or handling.
// BAD - Don't do this
Future<int> fetchData() async {
return 42;
}
void main() {
fetchData().whenComplete(() {
print('Fetch completed');
});
}
Solution:
// GOOD - Do this instead
Future<int> fetchData() async {
return 42;
}
void main() {
fetchData().whenComplete(() {
print('Fetch completed');
}).then((value) {
print('Fetched value: $value');
});
}
Why: Ignoring the return value of whenComplete can lead to missed opportunities for additional operations on the resulting Future. Always consider the flow of data and control through your asynchronous code.
2. Misusing `whenComplete` for Error Handling
Problem: Some beginners mistakenly believe that whenComplete is a way to handle errors, leading to the false assumption that it will catch exceptions.
// BAD - Don't do this
Future<void> fetchData() async {
throw Exception('Fetch error');
}
void main() {
fetchData().whenComplete(() {
print('Fetch completed'); // This will run even if there's an error
});
}
Solution:
// GOOD - Do this instead
Future<void> fetchData() async {
throw Exception('Fetch error');
}
void main() {
fetchData().then((_) {
// Handle successful data fetch
}).catchError((error) {
print('Error occurred: $error');
}).whenComplete(() {
print('Fetch completed, regardless of outcome');
});
}
Why: whenComplete executes regardless of whether the Future completed successfully or with an error. Use catchError for handling exceptions specifically, ensuring that your error handling is clear and effective.
3. Not Considering `whenComplete` Timing
Problem: Beginners might assume that whenComplete runs at the same time as the Future, not realizing it executes after the Future is completed.
// BAD - Don't do this
Future<void> fetchData() async {
await Future.delayed(Duration(seconds: 1));
print('Data fetched');
}
void main() {
fetchData().whenComplete(() {
print('Fetch completed immediately'); // Misleading timing
});
}
Solution:
// GOOD - Do this instead
Future<void> fetchData() async {
await Future.delayed(Duration(seconds: 1));
print('Data fetched');
}
void main() {
fetchData().whenComplete(() {
print('Fetch completed after data fetch');
});
}
Why: Understanding the timing of whenComplete is crucial for proper flow control in asynchronous programming. Misleading expectations can lead to confusion in code execution order.
4. Confusing `whenComplete` with `then`
Problem: Some beginners confuse whenComplete with then, thinking they serve the same purpose.
// BAD - Don't do this
Future<void> fetchData() async {
return;
}
void main() {
fetchData().then(() {
print('Fetch completed');
}).whenComplete(() {
print('This should be used for cleanup, not just completion');
});
}
Solution:
// GOOD - Do this instead
Future<void> fetchData() async {
return;
}
void main() {
fetchData().then(() {
print('Fetch completed');
}).whenComplete(() {
print('Cleanup or final actions can be performed here');
});
}
Why: then is used for handling results of a Future, while whenComplete is for cleanup actions or side effects that should occur regardless of success or failure. Understanding these distinctions can help maintain clarity in your code.
5. Not Using `whenComplete` for Cleanup Actions
Problem: Beginners might fail to utilize whenComplete for necessary cleanup actions after the completion of a Future, leading to resource leaks.
// BAD - Don't do this
Future<void> fetchData() async {
// Simulate fetching data
}
void main() {
fetchData().then((_) {
// Process data
});
// Forgetting to close resources or logs
}
Solution:
// GOOD - Do this instead
Future<void> fetchData() async {
// Simulate fetching data
}
void main() {
fetchData().then((_) {
// Process data
}).whenComplete(() {
print('Closing resources or logs here');
});
}
Why: Cleanup actions are critical in preventing resource leaks and ensuring that all necessary finalization occurs. Using whenComplete effectively can improve the stability and reliability of your applications.
Best Practices
1. Always Handle Errors Separately
Handling errors separately using catchError ensures that your application can robustly deal with failures without compromising overall flow.
fetchData()
.then((value) {
// Process value
})
.catchError((error) {
print('An error occurred: $error');
});
Tip: Always plan for potential errors in your asynchronous code, especially when dealing with network requests or file operations.
2. Use `whenComplete` for Cleanup
Utilizing whenComplete for cleanup actions, such as closing resources or logging, is essential for maintaining application hygiene.
fetchData().whenComplete(() {
// Cleanup actions here
});
Tip: Always consider what resources need to be released, such as file handles or network connections, and implement them in whenComplete.
3. Chain Futures Properly
Chaining multiple Futures with then and whenComplete can keep your code organized and maintain a clear control flow.
fetchData()
.then(processData)
.whenComplete(() => print('All operations completed.'));
Tip: Ensure each then handler returns a Future if you need to chain further operations, maintaining a consistent asynchronous flow.
4. Use `async/await` for Clarity
Using async/await syntax can greatly enhance readability and reduce nesting, making your asynchronous code easier to follow.
void main() async {
await fetchData();
print('Fetch completed');
}
Tip: Reserve whenComplete for cleanup or final actions; use async/await for most control flow scenarios to keep your code straightforward.
5. Document Your Asynchronous Code
Documenting what each future does, especially in complex chains, is crucial for maintainability.
/// Fetches data from the server
Future<void> fetchData() async {
// Implementation here
}
Tip: Use comments and documentation to explain the purpose of each asynchronous operation, especially in large applications.
6. Test Asynchronous Code Thoroughly
Testing your asynchronous code ensures that it behaves as expected under various conditions, including success and failure.
void main() {
test('fetchData returns expected result', () async {
final result = await fetchData();
expect(result, expectedValue);
});
}
Tip: Use testing frameworks that support asynchronous tests to ensure you cover all edge cases.
Key Points
| Point | Description |
|---|---|
Return Value of whenComplete |
Remember that whenComplete returns a new Future, which can be further chained. |
| Error Handling | Use catchError for handling exceptions instead of relying on whenComplete. |
Timing of whenComplete |
Understand that whenComplete runs after the Future is completed, not simultaneously. |
Distinguish whenComplete from then |
Use then for result handling and whenComplete for cleanup actions. |
| Cleanup Actions | Always use whenComplete for necessary cleanup to prevent resource leaks. |
| Chain Futures | Maintain an organized asynchronous control flow by chaining futures properly with then and whenComplete. |
Use async/await |
Enhance code readability and reduce nesting by using async/await syntax. |
| Document and Test | Thoroughly document and test asynchronous methods to ensure clarity and reliability. |