<h1 id="comprehensive-flutter-error-handling-best-practices-and-patterns">Comprehensive Flutter Error Handling: Best Practices and Patterns</h1> <p>Error handling is a crucial aspect of building robust Flutter applications. This guide covers comprehensive error handling strategies, patterns, and best practices to make your Flutter apps more reliable and user-friendly.</p> <h2 id="basic-error-handling">1. Basic Error Handling</h2> <h3 id="try-catch-blocks">Try-Catch Blocks</h3> <pre>Future<void> fetchData() async { try { final response = await http.get(Uri.parse('https://api.example.com/data')); if (response.statusCode == 200) { // Handle successful response } else { throw HttpException('Failed to load data'); } } on SocketException catch (e) { // Handle network errors showErrorDialog('No internet connection'); } on HttpException catch (e) { // Handle HTTP errors showErrorDialog(e.message); } on FormatException catch (e) { // Handle JSON parsing errors showErrorDialog('Invalid response format'); } catch (e) { // Handle other errors showErrorDialog('An unexpected error occurred'); } } </pre> <h3 id="custom-error-classes">Custom Error Classes</h3> <pre>class AppException implements Exception { final String message; final String? code; final dynamic details;
AppException(this.message, {this.code, this.details});
@override String toString() => 'AppException: $message (Code: $code)'; }
class NetworkException extends AppException { NetworkException([String? message]) : super(message ?? 'A network error occurred'); }
class ValidationException extends AppException { ValidationException(String message, {String? field}) : super(message, details: {'field': field}); } </pre> <h2 id="error-handling-in-state-management">2. Error Handling in State Management</h2> <h3 id="using-bloc-pattern">Using BLoC Pattern</h3> <pre>abstract class DataState class DataInitial extends DataState class DataLoading extends DataState class DataLoaded extends DataState { final List<dynamic> data; DataLoaded(this.data); } class DataError extends DataState { final String message; DataError(this.message); }
class DataBloc extends Cubit<DataState> { DataBloc() : super(DataInitial());
Future<void> fetchData() async { emit(DataLoading()); try { final result = await apiService.getData(); emit(DataLoaded(result)); } on NetworkException catch (e) { emit(DataError('Network error: $')); } catch (e) { emit(DataError('An unexpected error occurred')); } } } </pre> <h3 id="using-provider-pattern">Using Provider Pattern</h3> <pre>class DataProvider with ChangeNotifier { List<dynamic>? _data; String? _error; bool _loading = false;
bool get isLoading => _loading; List<dynamic>? get data => _data; String? get error => _error;
Future<void> fetchData() async { try { _loading = true; _error = null; notifyListeners();
_data = await apiService.getData();
_loading = false;
notifyListeners();
} catch (e) {
_loading = false;
_error = e.toString();
notifyListeners();
}
} } </pre> <h2 id="network-error-handling">3. Network Error Handling</h2> <h3 id="http-error-handling">HTTP Error Handling</h3> <pre>class ApiService { final dio = Dio();
Future<T> handleResponse<T>(Response response, T Function(dynamic) onSuccess) async { switch (response.statusCode) { case 200: return onSuccess(response.data); case 400: throw ValidationException('Invalid request'); case 401: throw AuthException('Unauthorized'); case 404: throw NotFoundException('Resource not found'); case 500: throw ServerException('Internal server error'); default: throw NetworkException('Network error: $'); } }
Future<T> safeApiCall<T>(Future<T> Function() apiCall) async { try { return await apiCall(); } on DioError catch (e) { switch (e.type) { case DioErrorType.connectTimeout: case DioErrorType.receiveTimeout: throw NetworkException('Connection timeout'); case DioErrorType.response: throw handleDioError(e.response); default: throw NetworkException('Network error occurred'); } } catch (e) { throw AppException('An unexpected error occurred'); } } } </pre> <h3 id="retry-mechanism">Retry Mechanism</h3> <pre>class RetryableRequest { final Future<dynamic> Function() request; final int maxAttempts; final Duration delay;
RetryableRequest({ required this.request, this.maxAttempts = 3, this.delay = const Duration(seconds: 1), });
Future<T> execute<T>() async { int attempts = 0; while (attempts < maxAttempts) { try { return await request() as T; } catch (e) { attempts++; if (attempts == maxAttempts) rethrow; await Future.delayed(delay * attempts); } } throw Exception('Max retry attempts reached'); } }
// Usage final result = await RetryableRequest( request: () => apiService.getData(), maxAttempts: 3, delay: Duration(seconds: 2), ).execute(); </pre> <h2 id="ui-error-handling">4. UI Error Handling</h2> <h3 id="error-boundary-widget">Error Boundary Widget</h3> <pre>class ErrorBoundary extends StatefulWidget { final Widget child; final Widget Function(FlutterErrorDetails) onError;
ErrorBoundary({ required this.child, required this.onError, });
@override _ErrorBoundaryState createState() => _ErrorBoundaryState(); }
class _ErrorBoundaryState extends State<ErrorBoundary> { FlutterErrorDetails? _error;
@override void initState() { super.initState(); FlutterError.onError = (FlutterErrorDetails details) { setState(() ); }; }
@override Widget build(BuildContext context) { if (_error != null) { return widget.onError(_error!); } return widget.child; } }
// Usage ErrorBoundary( onError: (error) => ErrorView( message: 'Something went wrong', onRetry: () => setState(() => _error = null), ), child: YourWidget(), ) </pre> <h3 id="error-widget">Error Widget</h3> <pre>class ErrorView extends StatelessWidget { final String message; final VoidCallback? onRetry;
const ErrorView({ required this.message, this.onRetry, });
@override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 48, color: Colors.red), SizedBox(height: 16), Text( message, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, ), if (onRetry != null) ...[ SizedBox(height: 16), ElevatedButton( onPressed: onRetry, child: Text('Retry'), ), ], ], ), ); } } </pre> <h2 id="form-validation">5. Form Validation</h2> <h3 id="form-validation-handler">Form Validation Handler</h3> <pre>class FormValidator { static String? validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Email is required'; } final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!emailRegex.hasMatch(value)) { return 'Invalid email format'; } return null; }
static String? validatePassword(String? value) { if (value == null || value.isEmpty) { return 'Password is required'; } if (value.length < 8) { return 'Password must be at least 8 characters'; } return null; } }
// Usage in Form Form( key: _formKey, child: Column( children: [ TextFormField( validator: FormValidator.validateEmail, decoration: InputDecoration( labelText: 'Email', errorStyle: TextStyle(color: Colors.red), ), ), TextFormField( validator: FormValidator.validatePassword, obscureText: true, decoration: InputDecoration( labelText: 'Password', errorStyle: TextStyle(color: Colors.red), ), ), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { // Form is valid, proceed with submission } }, child: Text('Submit'), ), ], ), ) </pre> <h2 id="global-error-handling">6. Global Error Handling</h2> <h3 id="error-handler-service">Error Handler Service</h3> <pre>class ErrorHandler { static final ErrorHandler _instance = ErrorHandler._internal(); factory ErrorHandler() => _instance; ErrorHandler._internal();
void initialize() { FlutterError.onError = (FlutterErrorDetails details) { FlutterError.presentError(details); _reportError(details.exception, details.stack); };
PlatformDispatcher.instance.onError = (error, stack) {
_reportError(error, stack);
return true;
};
}
Future<void> _reportError(dynamic error, StackTrace? stack) async { // Log error to analytics service await FirebaseCrashlytics.instance.recordError(error, stack);
// Log to console in debug mode
if (kDebugMode) {
print(&#39;Error: $error&#39;);
print(&#39;Stack trace: $stack&#39;);
}
}
void showErrorDialog(BuildContext context, String message) { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Error'), content: Text(message), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('OK'), ), ], ), ); } } </pre> <h2 id="best-practices">Best Practices</h2> <ol> <li><strong>Use Specific Error Types</strong></li> </ol> <pre>// Bad throw Exception('Something went wrong');
// Good throw NetworkException('Failed to connect to server'); </pre> <ol start="2"> <li><strong>Graceful Error Recovery</strong></li> </ol> <pre>Future<void> loadData() async { try { final data = await fetchData(); updateUI(data); } catch (e) { // Show error UI but keep app running showErrorView(); // Log error for debugging logError(e); } } </pre> <ol start="3"> <li><strong>Error Boundaries for Widget Trees</strong></li> </ol> <pre>MaterialApp( builder: (context, child) { return ErrorBoundary( onError: (error) => ErrorScreen(), child: child!, ); }, home: HomePage(), ) </pre> <ol start="4"> <li><strong>Consistent Error Handling</strong></li> </ol> <pre>// Create a centralized error handler class AppErrorHandler { static void handleError(BuildContext context, dynamic error) { String message; if (error is NetworkException) { message = 'Network error occurred'; } else if (error is ValidationException) else { message = 'An unexpected error occurred'; }
showErrorDialog(context, message);
} } </pre> <h2 id="conclusion">Conclusion</h2> <p>Effective error handling is crucial for building robust Flutter applications. Key takeaways:</p> <ol> <li>Use specific error types for different scenarios</li> <li>Implement proper error boundaries</li> <li>Handle network errors gracefully</li> <li>Validate form inputs thoroughly</li> <li>Implement global error handling</li> <li>Provide meaningful error messages to users</li> <li>Log errors for debugging and analytics</li> <li>Implement retry mechanisms where appropriate</li> <li>Keep the app functional even when errors occur</li> <li>Follow consistent error handling patterns</li> </ol> <p>Remember:</p> <ul> <li>Always provide user-friendly error messages</li> <li>Log errors for debugging purposes</li> <li>Implement proper error recovery mechanisms</li> <li>Use error boundaries to prevent app crashes</li> <li>Handle both expected and unexpected errors</li> <li>Test error scenarios thoroughly</li> </ul> <p>By following these practices, you can create more reliable and user-friendly Flutter applications that gracefully handle errors and provide better user experience.</p>