Runtime Errors in Flutter
Runtime errors are issues that occur during the execution of a Flutter application, even though the code compiles successfully. This comprehensive guide will help you understand, identify, and fix common runtime errors in Flutter.
1. Common Runtime Errors
1.1 Null Reference Errors
One of the most common runtime errors in Flutter is attempting to access properties or methods of null objects.
// ❌ Wrong: Unsafe null access String? name; print(name.length); // Error: Null check operator used on a null value // ✅ Correct: Safe null handling String? name; print(name?.length ?? 0); // Safely handles null with a default value // Alternative approaches if (name != null) { print(name.length); // Safe because of null check } // Using late initialization late String name; void initName() { name = fetchName(); // Initialize before use }
1.2 Index Out of Range Errors
These errors occur when trying to access list elements with invalid indices.
// ❌ Wrong: Unsafe list access List<String> items = ['a', 'b']; print(items[2]); // Error: RangeError (index): Invalid value // ✅ Correct: Safe list access List<String> items = ['a', 'b']; // Method 1: Check length if (items.length > 2) { print(items[2]); } // Method 2: Use elementAt with a default value print(items.elementAt(2, defaultValue: 'default')); // Method 3: Try-catch for specific handling try { print(items[2]); } on RangeError catch (e) { print('Index out of range: $e'); }
1.3 Type Cast Errors
Type cast errors occur when trying to cast objects to incompatible types.
// ❌ Wrong: Unsafe type casting dynamic data = 42; String text = data as String; // Error: TypeError // ✅ Correct: Safe type casting dynamic data = 42; // Method 1: Type check before casting if (data is String) { String text = data; print(text); } // Method 2: Try-catch with type checking try { String text = data.toString(); // Convert instead of casting print(text); } on TypeError catch (e) { print('Type cast error: $e'); }
2. State-Related Runtime Errors
2.1 Mounted Check Errors
class MyWidget extends StatefulWidget { @override _MyWidgetState createState() => _MyWidgetState(); } class _MyWidgetState extends State<MyWidget> { // ❌ Wrong: No mounted check in async operation Future<void> loadDataUnsafe() async { await Future.delayed(Duration(seconds: 1)); setState(() {}); // May cause error if widget is disposed } // ✅ Correct: With mounted check Future<void> loadDataSafe() async { await Future.delayed(Duration(seconds: 1)); if (mounted) { setState(() {}); } } @override Widget build(BuildContext context) { return Container(); } }
2.2 Build Context Errors
// ❌ Wrong: Using BuildContext after async gap Future<void> showDialogUnsafe(BuildContext context) async { await Future.delayed(Duration(seconds: 1)); showDialog(context: context, builder: (_) => AlertDialog()); } // ✅ Correct: Checking context mounting Future<void> showDialogSafe(BuildContext context) async { await Future.delayed(Duration(seconds: 1)); if (context.mounted) { showDialog(context: context, builder: (_) => AlertDialog()); } }
3. Resource Management Errors
3.1 Memory Leaks
class ResourceWidget extends StatefulWidget { @override _ResourceWidgetState createState() => _ResourceWidgetState(); } class _ResourceWidgetState extends State<ResourceWidget> { StreamSubscription? _subscription; // ❌ Wrong: No resource cleanup @override void initState() { super.initState(); _subscription = Stream.periodic(Duration(seconds: 1)) .listen((_) => print('Tick')); } // ✅ Correct: Proper resource cleanup @override void dispose() { _subscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } }
3.2 Image Loading Errors
// ❌ Wrong: No error handling for image loading Image.network('https://example.com/image.jpg') // ✅ Correct: With error handling Image.network( 'https://example.com/image.jpg', errorBuilder: (context, error, stackTrace) { return Container( child: Icon(Icons.error), ); }, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return CircularProgressIndicator(); }, )
4. Error Prevention and Handling
4.1 Global Error Handling
void main() { runZonedGuarded( () { WidgetsFlutterBinding.ensureInitialized(); FlutterError.onError = (FlutterErrorDetails details) { // Log error to service print('Flutter Error: ${details.exception}'); }; runApp(MyApp()); }, (error, stack) { // Handle errors not caught by Flutter print('Uncaught error: $error\n$stack'); }, ); }
4.2 Custom Error Handling Widget
class ErrorBoundary extends StatefulWidget { final Widget child; final Widget Function(FlutterErrorDetails) errorBuilder; const ErrorBoundary({ required this.child, required this.errorBuilder, }); @override _ErrorBoundaryState createState() => _ErrorBoundaryState(); } class _ErrorBoundaryState extends State<ErrorBoundary> { FlutterErrorDetails? _error; @override void initState() { super.initState(); FlutterError.onError = _handleFlutterError; } void _handleFlutterError(FlutterErrorDetails details) { setState(() { _error = details; }); } @override Widget build(BuildContext context) { if (_error != null) { return widget.errorBuilder(_error!); } return widget.child; } @override void dispose() { FlutterError.onError = null; super.dispose(); } } // Usage ErrorBoundary( errorBuilder: (error) => Center( child: Text('Something went wrong: ${error.exception}'), ), child: MyWidget(), )
4.3 Custom Error Widget
class CustomErrorWidget extends StatelessWidget { final String message; final VoidCallback? onRetry; const CustomErrorWidget({ required this.message, this.onRetry, }); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, color: Colors.red, size: 48, ), SizedBox(height: 16), Text( message, style: TextStyle( color: Colors.red, fontSize: 16, ), textAlign: TextAlign.center, ), if (onRetry != null) ...[ SizedBox(height: 16), ElevatedButton( onPressed: onRetry, child: Text('Retry'), ), ], ], ), ); } } // Usage FutureBuilder<Data>( future: fetchData(), builder: (context, snapshot) { if (snapshot.hasError) { return CustomErrorWidget( message: 'Failed to load data: ${snapshot.error}', onRetry: () { // Trigger data reload setState(() {}); }, ); } if (!snapshot.hasData) { return CircularProgressIndicator(); } return DataWidget(data: snapshot.data!); }, );
5. Debugging Runtime Errors
5.1 Using Debug Prints
// Add debug prints strategically void processData(dynamic data) { debugPrint('Processing data: $data'); // Debug print try { final result = complexOperation(data); debugPrint('Operation result: $result'); // Debug print } catch (e) { debugPrint('Error in processData: $e'); // Debug print rethrow; } }
5.2 Custom Error Reporting
class ErrorReporter { static void reportError( Object error, StackTrace stackTrace, { String? context, }) { // Log locally debugPrint('Error: $error'); debugPrint('Context: $context'); debugPrint('StackTrace: $stackTrace'); // Send to error reporting service // FirebaseCrashlytics.instance.recordError(error, stackTrace); } } // Usage try { riskyOperation(); } catch (e, s) { ErrorReporter.reportError( e, s, context: 'Performing risky operation', ); }
5. Testing Error Scenarios
5.1 Unit Testing Error Handling
void main() { test('Null safety handling', () { final dataService = DataService(); expect( () => dataService.processNullableData(null), throwsA(isA<ArgumentError>()), ); }); test('Index range handling', () { final list = ['a', 'b']; final listService = ListService(); expect( () => listService.getItem(list, 2), throwsA(isA<RangeError>()), ); }); }
5.2 Widget Testing Error States
void main() { testWidgets('Error widget displays correctly', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: CustomErrorWidget( message: 'Test error', onRetry: () {}, ), ), ); expect(find.text('Test error'), findsOneWidget); expect(find.byIcon(Icons.error_outline), findsOneWidget); expect(find.text('Retry'), findsOneWidget); }); testWidgets('Error boundary catches errors', (WidgetTester tester) async { await tester.pumpWidget( ErrorBoundary( errorBuilder: (details) => Text('Error caught'), child: Builder( builder: (context) => throw Exception('Test error'), ), ), ); await tester.pumpAndSettle(); expect(find.text('Error caught'), findsOneWidget); }); }
Conclusion
Effective runtime error handling in Flutter requires:
-
Proper Error Prevention
- Null safety practices
- Type checking
- Range validation
- Resource management
-
Error Handling Mechanisms
- Try-catch blocks
- Error boundaries
- Global error handlers
- Custom error widgets
-
State Management
- Proper widget lifecycle handling
- Safe async operations
- Resource cleanup
- Context validation
-
Testing
- Unit tests for error cases
- Widget tests for error states
- Integration tests
- Error boundary testing
Remember to:
- Always handle nullable values safely
- Implement proper error boundaries
- Clean up resources properly
- Test error scenarios thoroughly
- Use appropriate error handling patterns
- Consider user experience during errors
By following these practices, you can create robust Flutter applications that handle runtime errors gracefully and provide a better user experience.