Flutter Error Handling
•10 min read
This guide covers different error handling techniques and best practices for Flutter applications.
1. Basic Error Handling
1.1. Try-Catch Blocks
Future<void> fetchData() async { try { final response = await http.get(Uri.parse('https://api.example.com/data')); if (response.statusCode == 200) { // Process data } else { throw HttpException('Failed to load data: ${response.statusCode}'); } } catch (e) { if (e is SocketException) { // Handle network error print('Network error: ${e.message}'); } else if (e is HttpException) { // Handle HTTP error print('HTTP error: ${e.message}'); } else { // Handle other errors print('Unknown error: $e'); } } }
1.2. Custom Exceptions
class AppException implements Exception { final String message; final String? code; AppException(this.message, {this.code}); @override String toString() => 'AppException: $message${code != null ? ' (Code: $code)' : ''}'; } class NetworkException extends AppException { NetworkException(String message) : super(message, code: 'NETWORK_ERROR'); } class ValidationException extends AppException { ValidationException(String message) : super(message, code: 'VALIDATION_ERROR'); }
2. Error Boundaries
2.1. Error Boundary Widget
class ErrorBoundary extends StatefulWidget { final Widget child; const ErrorBoundary({Key? key, required this.child}) : super(key: key); @override _ErrorBoundaryState createState() => _ErrorBoundaryState(); } class _ErrorBoundaryState extends State<ErrorBoundary> { bool hasError = false; Object? error; @override Widget build(BuildContext context) { if (hasError) { return ErrorWidget(error!); } return widget.child; } @override void didCatchError(Object error, StackTrace stackTrace) { setState(() { hasError = true; this.error = error; }); // Log error print('Error caught: $error\n$stackTrace'); } }
2.2. Error Widget
class ErrorWidget extends StatelessWidget { final Object error; const ErrorWidget(this.error, {Key? key}) : 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( 'An error occurred', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 8), Text( error.toString(), textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 16), ElevatedButton( onPressed: () { // Retry or navigate back }, child: const Text('Retry'), ), ], ), ); } }
3. Global Error Handling
3.1. Error Handler
class ErrorHandler { static void handleError(Object error, StackTrace stackTrace) { // Log error print('Error: $error\n$stackTrace'); // Report to error tracking service FirebaseCrashlytics.instance.recordError(error, stackTrace); // Show error to user showErrorDialog(error); } static void showErrorDialog(Object error) { // Show error dialog showDialog( context: navigatorKey.currentContext!, builder: (context) => AlertDialog( title: const Text('Error'), content: Text(error.toString()), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('OK'), ), ], ), ); } }
3.2. Error Reporting
class ErrorReporter { static Future<void> initialize() async { await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); FlutterError.onError = (FlutterErrorDetails details) { FirebaseCrashlytics.instance.recordFlutterError(details); }; PlatformDispatcher.instance.onError = (Object error, StackTrace stack) { FirebaseCrashlytics.instance.recordError(error, stack); return true; }; } }
4. Form Validation
4.1. 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 'Please enter your email'; } if (!value.contains('@')) { return 'Please enter a valid email'; } return null; } String? _validatePassword(String? value) { if (value == null || value.isEmpty) { return 'Please enter your password'; } if (value.length < 6) { return 'Password must be at least 6 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'), ), TextFormField( controller: _passwordController, validator: _validatePassword, obscureText: true, decoration: const InputDecoration(labelText: 'Password'), ), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { // Process form } }, child: const Text('Login'), ), ], ), ); } }
5. Network Error Handling
5.1. Network Error Handler
class NetworkErrorHandler { static Future<T> handleNetworkCall<T>(Future<T> Function() call) async { try { return await call(); } on SocketException { throw NetworkException('No internet connection'); } on HttpException { throw NetworkException('Failed to load data'); } on FormatException { throw NetworkException('Invalid data format'); } catch (e) { throw NetworkException('An unexpected error occurred'); } } } class ApiService { Future<dynamic> fetchData() async { return await NetworkErrorHandler.handleNetworkCall(() async { final response = await http.get(Uri.parse('https://api.example.com/data')); if (response.statusCode == 200) { return jsonDecode(response.body); } throw HttpException('Failed to load data: ${response.statusCode}'); }); } }
5.2. 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); } } } }
6. Best Practices
-
Error Handling
- Use specific exception types
- Provide meaningful error messages
- Handle errors at appropriate levels
-
Error Reporting
- Log errors properly
- Report to error tracking services
- Include stack traces
-
User Experience
- Show user-friendly error messages
- Provide recovery options
- Maintain app state
-
Validation
- Validate input data
- Handle edge cases
- Provide clear feedback
-
Testing
- Test error scenarios
- Verify error handling
- Check error messages
Conclusion
Remember these key points:
- Handle errors gracefully
- Provide meaningful feedback
- Log and report errors
- Validate user input
- Test error scenarios
By following these practices, you can:
- Improve app reliability
- Enhance user experience
- Simplify debugging
- Maintain app stability
Keep improving your error handling in Flutter applications!