Back to Posts

Runtime Errors in Flutter

10 min read

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:

  1. Proper Error Prevention

    • Null safety practices
    • Type checking
    • Range validation
    • Resource management
  2. Error Handling Mechanisms

    • Try-catch blocks
    • Error boundaries
    • Global error handlers
    • Custom error widgets
  3. State Management

    • Proper widget lifecycle handling
    • Safe async operations
    • Resource cleanup
    • Context validation
  4. 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.