Back to Posts

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

  1. Error Prevention

    • Validate inputs
    • Check preconditions
    • Handle edge cases
    • Use proper types
  2. Error Handling

    • Use try-catch blocks
    • Create custom exceptions
    • Handle specific errors
    • Provide fallbacks
  3. Error Reporting

    • Log errors properly
    • Report to analytics
    • Include context
    • Track error rates
  4. User Experience

    • Show clear messages
    • Provide recovery options
    • Maintain state
    • Handle gracefully
  5. Debugging

    • Use proper logging
    • Include stack traces
    • Add error boundaries
    • Monitor performance

Conclusion

Remember these key points:

  1. Prevent errors
  2. Handle gracefully
  3. Report properly
  4. Improve UX
  5. 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!