Error Handling and Retry Logic in Flutter
Error Handling and Retry Logic in Flutter
What You’ll Learn
Learn how to gracefully handle errors in Flutter apps and implement smart retry mechanisms. You’ll discover patterns for catching exceptions, showing user-friendly error messages, and automatically retrying failed operations.
Why Error Handling Matters
Real-world apps face constant challenges: network failures, server errors, invalid user input, and unexpected data. Good error handling makes the difference between an app that crashes and one that recovers gracefully.
Common scenarios:
- Network requests that timeout
- API responses with unexpected formats
- Device offline when fetching data
- Parsing errors from malformed JSON
Basic Error Handling Patterns
Pattern 1: Try-Catch for Synchronous Code
String parseUserAge(String input) {
try {
final age = int.parse(input);
if (age < 0 || age > 150) {
throw FormatException('Age must be between 0 and 150');
}
return 'Valid age: $age';
} on FormatException catch (e) {
return 'Error: ${e.message}';
} catch (e) {
return 'Unexpected error: $e';
}
}
Pattern 2: Try-Catch for Async Operations
Future<User> fetchUser(String userId) async {
try {
final response = await http.get(
Uri.parse('https://api.example.com/users/$userId'),
);
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body));
} else if (response.statusCode == 404) {
throw UserNotFoundException('User $userId not found');
} else {
throw ApiException('Server error: ${response.statusCode}');
}
} on SocketException {
throw NetworkException('No internet connection');
} on TimeoutException {
throw NetworkException('Request timed out');
} on FormatException {
throw DataException('Invalid response format');
}
}
Example: Smart Retry Logic
Here’s a reusable retry function with exponential backoff:
import 'dart:async';
import 'package:flutter/material.dart';
// Custom exception types
class NetworkException implements Exception {
final String message;
NetworkException(this.message);
@override
String toString() => message;
}
// Retry configuration
class RetryConfig {
final int maxAttempts;
final Duration initialDelay;
final Duration maxDelay;
final double backoffMultiplier;
const RetryConfig({
this.maxAttempts = 3,
this.initialDelay = const Duration(seconds: 1),
this.maxDelay = const Duration(seconds: 10),
this.backoffMultiplier = 2.0,
});
}
// Generic retry function with exponential backoff
Future<T> retryOperation<T>({
required Future<T> Function() operation,
RetryConfig config = const RetryConfig(),
bool Function(Exception)? retryIf,
}) async {
int attempt = 0;
Duration delay = config.initialDelay;
while (true) {
attempt++;
try {
return await operation();
} on Exception catch (e) {
// Check if we should retry
final shouldRetry = retryIf?.call(e) ?? true;
if (attempt >= config.maxAttempts || !shouldRetry) {
rethrow; // Give up and throw the error
}
// Wait before retrying (exponential backoff)
print('Attempt $attempt failed: $e. Retrying in ${delay.inSeconds}s...');
await Future.delayed(delay);
// Increase delay for next attempt
delay = Duration(
milliseconds: (delay.inMilliseconds * config.backoffMultiplier).round(),
);
// Cap at maximum delay
if (delay > config.maxDelay) {
delay = config.maxDelay;
}
}
}
}
// Usage example
class UserRepository {
Future<User> getUser(String id) async {
return retryOperation(
operation: () async {
final response = await http.get(
Uri.parse('https://api.example.com/users/$id'),
).timeout(Duration(seconds: 5));
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body));
} else {
throw NetworkException('Failed to load user: ${response.statusCode}');
}
},
config: RetryConfig(
maxAttempts: 3,
initialDelay: Duration(seconds: 1),
),
retryIf: (e) {
// Only retry network errors, not validation errors
return e is NetworkException || e is TimeoutException;
},
);
}
}
UI Pattern: Error State Management
Display loading, error, and success states elegantly:
class UserScreen extends StatefulWidget {
@override
State<UserScreen> createState() => _UserScreenState();
}
class _UserScreenState extends State<UserScreen> {
bool _isLoading = false;
String? _errorMessage;
User? _user;
final _repository = UserRepository();
@override
void initState() {
super.initState();
_loadUser();
}
Future<void> _loadUser() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final user = await _repository.getUser('123');
setState(() {
_user = user;
_isLoading = false;
});
} on NetworkException catch (e) {
setState(() {
_errorMessage = e.toString();
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'An unexpected error occurred';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User Profile')),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_isLoading) {
return Center(child: CircularProgressIndicator());
}
if (_errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red),
SizedBox(height: 16),
Text(_errorMessage!, style: TextStyle(fontSize: 16)),
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _loadUser,
icon: Icon(Icons.refresh),
label: Text('Retry'),
),
],
),
);
}
if (_user != null) {
return Center(
child: Text('Hello, ${_user!.name}!'),
);
}
return Center(child: Text('No data'));
}
}
Try It Yourself
Enhance the retry mechanism:
- Add a progress indicator showing retry attempts (“Attempt 2 of 3”)
- Implement a circuit breaker that stops retries after consecutive failures
- Add offline detection to avoid retrying when device has no connection
- Create a custom error widget that shows different messages for different error types
Challenge: Build a file upload feature with retry logic that:
- Automatically retries on network failure
- Shows upload progress
- Allows manual retry on failure
- Queues uploads to retry when connection returns
Tip of the Day
Global Error Handling: Catch uncaught errors with FlutterError.onError and runZonedGuarded:
void main() {
FlutterError.onError = (details) {
// Log to crash reporting service
print('Flutter error: ${details.exception}');
};
runZonedGuarded(
() => runApp(MyApp()),
(error, stack) {
// Catch async errors
print('Uncaught error: $error');
},
);
}
Testing Errors: Simulate errors in development using mock services. This helps you design better error UIs and recovery flows before users encounter real failures.