How do you handle app crashes?
Answer
Overview
Handling app crashes is critical for maintaining a good user experience and debugging issues in production. Flutter provides several mechanisms to catch, log, and report crashes from both Dart code and native platform code.
1. Catching Dart Exceptions
FlutterError.onError
Catches errors that occur during the Flutter framework's execution (e.g., widget build errors).
dartvoid main() { FlutterError.onError = (FlutterErrorDetails details) { // Log error details print('Flutter Error: ${details.exception}'); print('Stack Trace: ${details.stack}'); // Send to crash reporting service // FirebaseCrashlytics.instance.recordFlutterError(details); }; runApp(MyApp()); }
2. Catching Async Exceptions
Use
runZonedGuardeddartimport 'dart:async'; void main() { runZonedGuarded( () { // Setup Flutter error handler FlutterError.onError = (details) { print('Flutter Error: ${details.exception}'); // Report to crash service }; runApp(MyApp()); }, (error, stackTrace) { // Catches all uncaught async errors print('Uncaught Error: $error'); print('Stack Trace: $stackTrace'); // Report to crash service }, ); }
3. Firebase Crashlytics Integration
Firebase Crashlytics is the most popular crash reporting solution for Flutter.
Setup
yaml# pubspec.yaml dependencies: firebase_core: ^2.24.0 firebase_crashlytics: ^3.4.9
Implementation
dartimport 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'dart:async'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Firebase await Firebase.initializeApp(); // Pass all uncaught errors to Crashlytics FlutterError.onError = (errorDetails) { FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails); }; // Pass all uncaught async errors to Crashlytics PlatformDispatcher.instance.onError = (error, stack) { FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); return true; }; runApp(MyApp()); }
Manual Error Logging
darttry { await someRiskyOperation(); } catch (e, stackTrace) { // Log non-fatal error await FirebaseCrashlytics.instance.recordError( e, stackTrace, reason: 'Error during risky operation', fatal: false, ); }
Custom Keys and User Info
dart// Set user identifier FirebaseCrashlytics.instance.setUserIdentifier('user_12345'); // Add custom keys for debugging FirebaseCrashlytics.instance.setCustomKey('page', 'checkout'); FirebaseCrashlytics.instance.setCustomKey('cart_value', 99.99); // Log custom messages FirebaseCrashlytics.instance.log('User clicked checkout button');
4. Sentry Integration
Sentry is another popular crash reporting tool.
yaml# pubspec.yaml dependencies: sentry_flutter: ^7.14.0
dartimport 'package:sentry_flutter/sentry_flutter.dart'; Future<void> main() async { await SentryFlutter.init( (options) { options.dsn = 'https://your-sentry-dsn.io/project-id options.tracesSampleRate = 1.0; // Performance monitoring }, appRunner: () => runApp(MyApp()), ); } // Manual error reporting try { await riskyOperation(); } catch (e, stackTrace) { await Sentry.captureException(e, stackTrace: stackTrace); }
5. Custom Error Screen
Show a friendly error screen instead of a red error screen in production.
dartvoid main() { // In production, show custom error UI ErrorWidget.builder = (FlutterErrorDetails details) { return MaterialApp( home: Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 80, color: Colors.red), SizedBox(height: 16), Text( 'Something went wrong!', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), SizedBox(height: 8), Text('Please restart the app.'), ], ), ), ), ); }; runApp(MyApp()); }
6. Platform-Specific Crash Handling
iOS (Native Crashes)
Firebase Crashlytics automatically handles native iOS crashes when configured.
Android (Native Crashes)
Firebase Crashlytics automatically handles native Android crashes when configured.
Configure in textAndroidManifest.xml
AndroidManifest.xmlxml<application> <meta-data android:name="firebase_crashlytics_collection_enabled" android:value="true" /> </application>
7. Testing Crash Reporting
Force a Crash for Testing
dart// In development only ElevatedButton( onPressed: () { // Force crash for testing FirebaseCrashlytics.instance.crash(); }, child: Text('Test Crash'), ) // Throw test error throw Exception('Test error for Crashlytics');
8. Best Practices
| Practice | Description |
|---|---|
| Catch all errors | Use text text |
| Log context | Add custom keys (user ID, screen, action) |
| Test in debug mode | Test crash reporting before production |
| Don't catch everything | Let fatal errors crash (easier to debug) |
| Monitor dashboard | Regularly check Crashlytics/Sentry dashboard |
| Prioritize crashes | Fix high-frequency crashes first |
| Version tracking | Tag crashes with app version |
9. Environment-Specific Handling
dartvoid main() { const bool isProduction = bool.fromEnvironment('dart.vm.product'); if (isProduction) { // Production: Send to crash service FlutterError.onError = (details) { FirebaseCrashlytics.instance.recordFlutterFatalError(details); }; } else { // Development: Print to console FlutterError.onError = (details) { print('Flutter Error: ${details.exception}'); print('Stack: ${details.stack}'); }; } runApp(MyApp()); }
10. Complete Example
dartimport 'dart:async'; import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); // Flutter framework errors FlutterError.onError = (errorDetails) { FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails); }; // Async errors PlatformDispatcher.instance.onError = (error, stack) { FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); return true; }; // Custom error widget ErrorWidget.builder = (details) { return MaterialApp( home: Scaffold( body: Center( child: Text('An error occurred. Please restart the app.'), ), ), ); }; runApp(MyApp()); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Crash Handling Demo', home: HomePage(), ); } } class HomePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Crash Handling')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () { // This will be caught by FlutterError.onError throw Exception('Test Flutter Error'); }, child: Text('Throw Sync Error'), ), SizedBox(height: 16), ElevatedButton( onPressed: () async { // This will be caught by PlatformDispatcher.onError await Future.delayed(Duration(seconds: 1)); throw Exception('Test Async Error'); }, child: Text('Throw Async Error'), ), SizedBox(height: 16), ElevatedButton( onPressed: () { // Manual error logging try { // Simulate error int.parse('invalid'); } catch (e, stack) { FirebaseCrashlytics.instance.recordError( e, stack, reason: 'Failed to parse number', ); } }, child: Text('Log Manual Error'), ), ], ), ), ); } }
Summary
Handle crashes using FlutterError.onError for framework errors, PlatformDispatcher.onError for async errors, and integrate Firebase Crashlytics or Sentry for production monitoring. Always add context (user ID, custom keys) for easier debugging.
Learn more at Firebase Crashlytics and Sentry Flutter.