Back to Posts

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

  1. Error Handling

    • Use specific exception types
    • Provide meaningful error messages
    • Handle errors at appropriate levels
  2. Error Reporting

    • Log errors properly
    • Report to error tracking services
    • Include stack traces
  3. User Experience

    • Show user-friendly error messages
    • Provide recovery options
    • Maintain app state
  4. Validation

    • Validate input data
    • Handle edge cases
    • Provide clear feedback
  5. Testing

    • Test error scenarios
    • Verify error handling
    • Check error messages

Conclusion

Remember these key points:

  1. Handle errors gracefully
  2. Provide meaningful feedback
  3. Log and report errors
  4. Validate user input
  5. 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!