Flutter Error Handling Best Practices
•12 min read
This guide covers different error handling techniques and strategies for Flutter applications.
1. Basic Error Handling
Try-Catch Blocks
class DataService { Future<String> fetchData() async { try { final response = await http.get(Uri.parse('https://api.example.com/data')); if (response.statusCode == 200) { return response.body; } else { throw HttpException('Failed to load data: ${response.statusCode}'); } } on SocketException catch (e) { throw NetworkException('No internet connection: $e'); } on FormatException catch (e) { throw DataFormatException('Invalid data format: $e'); } catch (e) { throw UnknownException('An unknown error occurred: $e'); } } } // Usage try { final data = await dataService.fetchData(); // Process data } on NetworkException catch (e) { // Handle network error showErrorSnackBar('Please check your internet connection'); } on DataFormatException catch (e) { // Handle format error showErrorSnackBar('Invalid data received'); } catch (e) { // Handle unknown errors showErrorSnackBar('Something went wrong'); }
Custom Exceptions
class NetworkException implements Exception { final String message; NetworkException(this.message); } class DataFormatException implements Exception { final String message; DataFormatException(this.message); } class UnknownException implements Exception { final String message; UnknownException(this.message); } // Usage void handleError(Exception e) { if (e is NetworkException) { // Handle network error } else if (e is DataFormatException) { // Handle format error } else { // Handle unknown error } }
2. Error Boundaries
Error Boundary Widget
class ErrorBoundary extends StatefulWidget { final Widget child; final Widget Function(BuildContext, Object) errorBuilder; const ErrorBoundary({ Key? key, required this.child, required this.errorBuilder, }) : super(key: key); @override _ErrorBoundaryState createState() => _ErrorBoundaryState(); } class _ErrorBoundaryState extends State<ErrorBoundary> { Object? _error; @override Widget build(BuildContext context) { if (_error != null) { return widget.errorBuilder(context, _error!); } return widget.child; } void _handleError(Object error, StackTrace stackTrace) { setState(() { _error = error; }); // Log error debugPrint('Error: $error\nStack trace: $stackTrace'); } } // Usage ErrorBoundary( errorBuilder: (context, error) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48), const SizedBox(height: 16), Text('Error: $error'), ElevatedButton( onPressed: () { // Retry or navigate back }, child: const Text('Retry'), ), ], ), ); }, child: MyWidget(), )
Error Widget
class ErrorWidget extends StatelessWidget { final String message; final VoidCallback? onRetry; const ErrorWidget({ Key? key, required this.message, this.onRetry, }) : super(key: key); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48, color: Colors.red), const SizedBox(height: 16), Text( message, textAlign: TextAlign.center, style: const TextStyle(color: Colors.red), ), if (onRetry != null) ...[ const SizedBox(height: 16), ElevatedButton( onPressed: onRetry, child: const Text('Retry'), ), ], ], ), ); } }
3. Global Error Handling
Error Handler
class ErrorHandler { static void handleError(Object error, StackTrace stackTrace) { // Log error debugPrint('Error: $error\nStack trace: $stackTrace'); // Report to analytics FirebaseCrashlytics.instance.recordError(error, stackTrace); // Show error to user if (error is NetworkException) { showErrorSnackBar('Please check your internet connection'); } else if (error is DataFormatException) { showErrorSnackBar('Invalid data received'); } else { showErrorSnackBar('Something went wrong'); } } } // Usage void main() { FlutterError.onError = (FlutterErrorDetails details) { ErrorHandler.handleError(details.exception, details.stack!); }; runApp(const MyApp()); }
Error Reporting
class ErrorReporter { static Future<void> reportError( Object error, StackTrace stackTrace, { String? context, }) async { // Report to Firebase Crashlytics await FirebaseCrashlytics.instance.recordError( error, stackTrace, reason: context, ); // Report to Sentry await Sentry.captureException( error, stackTrace: stackTrace, hint: context, ); // Log to console debugPrint('Error: $error\nStack trace: $stackTrace\nContext: $context'); } }
4. Form Validation
Form Validation
class LoginForm extends StatefulWidget { @override _LoginFormState createState() => _LoginFormState(); } class _LoginFormState extends State<LoginForm> { final _formKey = GlobalKey<FormState>(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Email is required'; } if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { return 'Please enter a valid email'; } return null; } 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; } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( controller: _emailController, validator: _validateEmail, decoration: const InputDecoration( labelText: 'Email', hintText: 'Enter your email', ), ), TextFormField( controller: _passwordController, validator: _validatePassword, obscureText: true, decoration: const InputDecoration( labelText: 'Password', hintText: 'Enter your password', ), ), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { // Process form } }, child: const Text('Login'), ), ], ), ); } }
Custom Form Field
class CustomFormField extends StatelessWidget { final String label; final String? Function(String?)? validator; final TextEditingController controller; final bool obscureText; final TextInputType? keyboardType; final Widget? prefixIcon; final Widget? suffixIcon; const CustomFormField({ Key? key, required this.label, required this.validator, required this.controller, this.obscureText = false, this.keyboardType, this.prefixIcon, this.suffixIcon, }) : super(key: key); @override Widget build(BuildContext context) { return TextFormField( controller: controller, obscureText: obscureText, keyboardType: keyboardType, validator: validator, decoration: InputDecoration( labelText: label, prefixIcon: prefixIcon, suffixIcon: suffixIcon, border: const OutlineInputBorder(), errorStyle: const TextStyle(color: Colors.red), ), ); } }
5. Network Error Handling
Network Error Handler
class NetworkErrorHandler { static Future<T> handleNetworkCall<T>({ required Future<T> Function() call, required T Function() fallback, }) async { try { return await call(); } on SocketException catch (e) { debugPrint('Network error: $e'); return fallback(); } on TimeoutException catch (e) { debugPrint('Timeout error: $e'); return fallback(); } catch (e) { debugPrint('Unknown error: $e'); return fallback(); } } } // Usage final result = await NetworkErrorHandler.handleNetworkCall( call: () => api.getData(), fallback: () => cachedData, );
Retry Mechanism
class RetryHandler { static Future<T> withRetry<T>({ required Future<T> Function() operation, int maxRetries = 3, Duration delay = const Duration(seconds: 1), }) async { int attempts = 0; while (true) { try { return await operation(); } catch (e) { attempts++; if (attempts >= maxRetries) { rethrow; } await Future.delayed(delay * attempts); } } } } // Usage final result = await RetryHandler.withRetry( operation: () => api.getData(), maxRetries: 3, delay: const Duration(seconds: 1), );
Best Practices
-
Error Prevention
- Validate inputs
- Check preconditions
- Handle edge cases
- Use proper types
-
Error Handling
- Use try-catch blocks
- Create custom exceptions
- Handle specific errors
- Provide fallbacks
-
Error Reporting
- Log errors properly
- Report to analytics
- Include context
- Track error rates
-
User Experience
- Show clear messages
- Provide recovery options
- Maintain state
- Handle gracefully
-
Debugging
- Use proper logging
- Include stack traces
- Add error boundaries
- Monitor performance
Conclusion
Remember these key points:
- Prevent errors
- Handle gracefully
- Report properly
- Improve UX
- Debug effectively
By following these practices, you can:
- Build robust apps
- Improve user experience
- Reduce crashes
- Debug efficiently
Keep handling errors in your Flutter applications!