Advanced Network Error Handling in Flutter
Network errors are inevitable in mobile applications. This comprehensive guide will show you how to implement robust error handling strategies in Flutter to ensure a smooth user experience even when network issues occur.
Common Network Errors and Solutions
1. Connection Timeout
When the server takes too long to respond:
// Problem: Basic HTTP request without timeout final response = await http.get(Uri.parse('https://api.example.com/data'));
Solution with proper timeout handling:
Future<dynamic> fetchDataWithTimeout() async { try { final response = await http.get( Uri.parse('https://api.example.com/data'), ).timeout( const Duration(seconds: 10), onTimeout: () { throw TimeoutException('Server took too long to respond'); }, ); return jsonDecode(response.body); } on TimeoutException catch (e) { // Handle timeout specifically throw NetworkException('Connection timeout: Please try again'); } on SocketException catch (e) { // Handle no internet connection throw NetworkException('No internet connection'); } catch (e) { throw NetworkException('Failed to fetch data: $e'); } }
2. Error Response Handling
Create a robust error handling system:
class NetworkException implements Exception { final String message; final int? statusCode; final dynamic data; final String? errorCode; final DateTime timestamp; NetworkException( this.message, { this.statusCode, this.data, this.errorCode, DateTime? timestamp, }) : timestamp = timestamp ?? DateTime.now(); @override String toString() => 'NetworkException: $message'; } class ApiService { final _client = Dio(); // Using Dio for better error handling Future<T> handleResponse<T>(Response response, T Function(dynamic) parser) { switch (response.statusCode) { case 200: case 201: return Future.value(parser(response.data)); case 400: throw NetworkException('Bad request', statusCode: 400, data: response.data); case 401: throw NetworkException('Unauthorized', statusCode: 401); case 403: throw NetworkException('Forbidden', statusCode: 403); case 404: throw NetworkException('Not found', statusCode: 404); case 500: throw NetworkException('Server error', statusCode: 500); default: throw NetworkException( 'Unexpected error occurred', statusCode: response.statusCode, ); } } }
3. Implementing Retry Mechanism
Create a retry utility for failed requests:
class RetryOptions { final int maxAttempts; final Duration delay; final bool exponentialBackoff; final List<int>? retryableStatusCodes; const RetryOptions({ this.maxAttempts = 3, this.delay = const Duration(seconds: 1), this.exponentialBackoff = true, this.retryableStatusCodes, }); Future<T> retry<T>(Future<T> Function() operation) async { int attempts = 0; while (true) { try { attempts++; return await operation(); } catch (error) { if (attempts >= maxAttempts) { rethrow; } // Check if error is retryable if (error is NetworkException && retryableStatusCodes != null && !retryableStatusCodes!.contains(error.statusCode)) { rethrow; } final waitTime = exponentialBackoff ? delay * math.pow(2, attempts - 1) : delay; await Future.delayed(waitTime); } } } } // Usage example final retryOptions = RetryOptions( maxAttempts: 3, retryableStatusCodes: [408, 429, 500, 502, 503, 504], ); final result = await retryOptions.retry(() => fetchData());
Advanced Error Handling Techniques
1. Circuit Breaker Pattern
Implement a circuit breaker to prevent cascading failures:
class CircuitBreaker { final int failureThreshold; final Duration resetTimeout; final Duration halfOpenTimeout; int _failureCount = 0; DateTime? _lastFailureTime; bool _isOpen = false; bool _isHalfOpen = false; CircuitBreaker({ this.failureThreshold = 3, this.resetTimeout = const Duration(seconds: 30), this.halfOpenTimeout = const Duration(seconds: 5), }); Future<T> execute<T>(Future<T> Function() operation) async { if (_isOpen) { if (_lastFailureTime != null && DateTime.now().difference(_lastFailureTime!) > resetTimeout) { _isOpen = false; _isHalfOpen = true; _lastFailureTime = DateTime.now(); } else { throw CircuitBreakerException('Circuit breaker is open'); } } try { final result = await operation(); _reset(); return result; } catch (e) { _failureCount++; _lastFailureTime = DateTime.now(); if (_failureCount >= failureThreshold) { _isOpen = true; _isHalfOpen = false; } rethrow; } } void _reset() { _failureCount = 0; _lastFailureTime = null; _isOpen = false; _isHalfOpen = false; } } class CircuitBreakerException implements Exception { final String message; final DateTime timestamp; CircuitBreakerException(this.message) : timestamp = DateTime.now(); }
2. Offline Support with Caching
Implement offline support using Hive for local storage:
class CachedApiService { final ApiService _apiService; final Box _cacheBox; final Duration _cacheDuration; final bool _enableOfflineMode; CachedApiService({ required ApiService apiService, required Box cacheBox, Duration cacheDuration = const Duration(hours: 1), bool enableOfflineMode = true, }) : _apiService = apiService, _cacheBox = cacheBox, _cacheDuration = cacheDuration, _enableOfflineMode = enableOfflineMode; Future<T> get<T>(String key, Future<T> Function() fetchData) async { try { // Try to get fresh data final data = await fetchData(); await _cacheBox.put(key, { 'data': data, 'timestamp': DateTime.now().millisecondsSinceEpoch, }); return data; } catch (e) { if (!_enableOfflineMode) rethrow; // If network fails, try to get cached data final cached = _cacheBox.get(key); if (cached != null) { final timestamp = cached['timestamp'] as int; final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestamp); if (DateTime.now().difference(cacheTime) < _cacheDuration) { return cached['data'] as T; } } rethrow; } } }
3. Error Logging and Analytics
Implement comprehensive error logging:
class ErrorLogger { final FirebaseCrashlytics _crashlytics; final AnalyticsService _analytics; final bool _enableLogging; ErrorLogger( this._crashlytics, this._analytics, { bool enableLogging = true, }) : _enableLogging = enableLogging; Future<void> logError(dynamic error, StackTrace? stackTrace) async { if (!_enableLogging) return; if (error is NetworkException) { await _crashlytics.recordError( error, stackTrace, reason: 'Network Error', information: [ 'statusCode: ${error.statusCode}', 'errorCode: ${error.errorCode}', 'timestamp: ${error.timestamp}', ], ); await _analytics.logEvent('network_error', { 'status_code': error.statusCode, 'error_code': error.errorCode, 'message': error.message, 'timestamp': error.timestamp.toIso8601String(), }); } else { await _crashlytics.recordError(error, stackTrace); } } }
Error UI Handling
1. Advanced Error Widget
Create a reusable error widget with different states:
class NetworkErrorWidget extends StatelessWidget { final String message; final VoidCallback onRetry; final NetworkErrorType type; final bool showRetry; final bool showDetails; const NetworkErrorWidget({ required this.message, required this.onRetry, this.type = NetworkErrorType.generic, this.showRetry = true, this.showDetails = false, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildIcon(), SizedBox(height: 16), Text( message, textAlign: TextAlign.center, style: TextStyle(fontSize: 16), ), if (showDetails) ...[ SizedBox(height: 8), Text( _getDetails(), textAlign: TextAlign.center, style: TextStyle(fontSize: 12, color: Colors.grey), ), ], if (showRetry) ...[ SizedBox(height: 16), ElevatedButton.icon( onPressed: onRetry, icon: Icon(Icons.refresh), label: Text('Retry'), ), ], ], ), ); } Widget _buildIcon() { switch (type) { case NetworkErrorType.noConnection: return Icon(Icons.wifi_off, size: 48, color: Colors.orange); case NetworkErrorType.timeout: return Icon(Icons.timer_off, size: 48, color: Colors.red); case NetworkErrorType.serverError: return Icon(Icons.error_outline, size: 48, color: Colors.red); default: return Icon(Icons.error_outline, size: 48, color: Colors.grey); } } String _getDetails() { switch (type) { case NetworkErrorType.noConnection: return 'Please check your internet connection and try again'; case NetworkErrorType.timeout: return 'The server took too long to respond'; case NetworkErrorType.serverError: return 'There was a problem with the server'; default: return 'An unexpected error occurred'; } } } enum NetworkErrorType { generic, noConnection, timeout, serverError, }
2. Integration with State Management
Example using Riverpod:
final dataProvider = StateNotifierProvider<DataNotifier, AsyncValue<Data>>((ref) { return DataNotifier(ref.watch(apiServiceProvider)); }); class DataNotifier extends StateNotifier<AsyncValue<Data>> { final ApiService _apiService; final CircuitBreaker _circuitBreaker; final ErrorLogger _errorLogger; DataNotifier(this._apiService) : _circuitBreaker = CircuitBreaker(), _errorLogger = ErrorLogger(FirebaseCrashlytics.instance, AnalyticsService()), super(const AsyncValue.loading()); Future<void> fetchData() async { state = const AsyncValue.loading(); try { final data = await _circuitBreaker.execute(() => _apiService.fetchData() ); state = AsyncValue.data(data); } on NetworkException catch (e, stack) { await _errorLogger.logError(e, stack); state = AsyncValue.error(e, stack); } catch (e, stack) { await _errorLogger.logError(e, stack); state = AsyncValue.error(e, stack); } } }
3. Implementation in Widget
class DataScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final dataAsync = ref.watch(dataProvider); return dataAsync.when( loading: () => Center(child: CircularProgressIndicator()), error: (error, stack) { if (error is NetworkException) { return NetworkErrorWidget( message: error.message, type: _getErrorType(error), onRetry: () => ref.read(dataProvider.notifier).fetchData(), showDetails: true, ); } return NetworkErrorWidget( message: 'An unexpected error occurred', onRetry: () => ref.read(dataProvider.notifier).fetchData(), ); }, data: (data) => ListView.builder( itemCount: data.length, itemBuilder: (context, index) { return ListTile( title: Text(data[index].title), ); }, ), ); } NetworkErrorType _getErrorType(NetworkException error) { if (error.statusCode == null) return NetworkErrorType.noConnection; if (error.statusCode == 408) return NetworkErrorType.timeout; if (error.statusCode! >= 500) return NetworkErrorType.serverError; return NetworkErrorType.generic; } }
Best Practices
- Connection Monitoring
class ConnectivityService { final Connectivity _connectivity = Connectivity(); final _controller = StreamController<ConnectivityResult>(); final _subscriptions = <StreamSubscription>[]; Stream<ConnectivityResult> get onConnectivityChanged => _controller.stream; Future<void> initialize() async { _subscriptions.add( _connectivity.onConnectivityChanged.listen((result) { _controller.add(result); }), ); } Future<bool> isConnected() async { final result = await _connectivity.checkConnectivity(); return result != ConnectivityResult.none; } void dispose() { for (final subscription in _subscriptions) { subscription.cancel(); } _controller.close(); } }
-
Error Recovery Strategies
- Implement exponential backoff for retries
- Use circuit breakers for critical services
- Cache responses for offline support
- Provide clear error messages to users
- Log errors for debugging and analytics
-
Performance Considerations
- Set appropriate timeouts
- Implement request cancellation
- Use connection pooling
- Optimize payload size
- Implement proper caching
-
Security Best Practices
- Validate all responses
- Handle sensitive data properly
- Implement proper authentication
- Use HTTPS for all requests
- Sanitize error messages
Conclusion
Effective network error handling is crucial for creating robust Flutter applications. By implementing these advanced techniques and following best practices, you can ensure your app provides a smooth user experience even in challenging network conditions.
Next Steps
- Implement comprehensive error logging
- Add offline support with caching
- Set up monitoring and analytics
- Create automated tests for error scenarios
- Document error handling procedures
Remember to:
- Test error scenarios thoroughly
- Monitor error rates in production
- Update error handling as needed
- Keep error messages user-friendly
- Follow security best practices
Happy coding with robust error handling!