Crash reporting in Flutter with Firebase Crashlytics
By Ann Tech · 19 October 2025
Firebase Crashlytics automatically captures unhandled crashes and non-fatal errors in production Flutter apps. It symbolication, device info, and breadcrumbs give you the context to reproduce and fix bugs that only appear in production.
Setup
dependencies:
firebase_crashlytics: ^4.0.0
firebase_core: ^3.0.0
Integration in main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Pass all Flutter framework errors to Crashlytics
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
// Pass async errors not caught by the Flutter framework
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true; // Return true to prevent propagation
};
runApp(const MyApp());
}
Recording non-fatal errors
Non-fatal errors appear in Crashlytics but don't count toward your crash-free users metric:
try {
final order = await orderRepo.getOrder(orderId);
// ...
} catch (e, stackTrace) {
// Non-fatal: log and continue
await FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
fatal: false,
reason: 'Failed to load order screen',
information: ['orderId: $orderId', 'userId: ${user.id}'],
);
// Show user-friendly error
emit(OrderError('Could not load order'));
}
Adding context
Crashlytics shows user identifier and custom keys in crash reports:
// After sign-in:
await FirebaseCrashlytics.instance.setUserIdentifier(user.id);
// Custom keys for filtering
await FirebaseCrashlytics.instance.setCustomKey('user_plan', user.plan);
await FirebaseCrashlytics.instance.setCustomKey('app_build', AppConfig.buildNumber);
await FirebaseCrashlytics.instance.setCustomKey('flavor', AppConfig.flavor);
// Log significant events as breadcrumbs (shown in crash report)
FirebaseCrashlytics.instance.log('User tapped checkout');
FirebaseCrashlytics.instance.log('Payment started: ${order.total}');
Crash-free users metric
Crashlytics calculates crash-free users: (users without crashes / total users) * 100.
In Firebase Console → Crashlytics → Dashboard:
- Aim for > 99.5% crash-free rate for production apps
- Sort issues by "Users affected" to prioritize
- Filter by OS version, device, or app version
Obfuscated stack traces
Obfuscated builds produce unreadable crash reports without symbol files. Set up automatic dSYM upload:
Android: The firebase-crashlytics Gradle plugin uploads symbols automatically:
// android/app/build.gradle.kts
plugins {
id("com.google.firebase.crashlytics")
}
android {
buildTypes {
release {
// Upload split debug info
configure<com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension> {
nativeSymbolUploadEnabled = true
strippedNativeLibsDir = "build/app/intermediates/stripped_native_libs"
}
}
}
}
iOS: Xcode automatically uploads dSYMs when building for the App Store with Bitcode disabled.
Dart obfuscation: Upload the --split-debug-info output:
flutter build apk --obfuscate --split-debug-info=./debug-info
# Upload to Crashlytics
firebase crashlytics:symbols:upload \
--app=1:123:android:abc123 \
./debug-info
Testing Crashlytics
Force a crash to verify the integration:
// Only in debug/testing — never in production code
if (kDebugMode) {
ElevatedButton(
onPressed: () => FirebaseCrashlytics.instance.crash(),
child: const Text('Test Crash'),
);
}
After the crash:
- Re-open the app (crashes are sent on next launch)
- Wait ~1 minute for Firebase to process
- Check Crashlytics dashboard
AlertPolicies for crash rate
Set up Firebase Performance Monitoring or Crashlytics email alerts:
- Firebase Console → Crashlytics → Email alerts
- Get notified when:
- New issue is introduced
- Existing issue gets a spike
- Crash-free users drops below threshold
Disabling in development
// Disable Crashlytics in debug mode to keep the dashboard clean
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(
!kDebugMode,
);
Common pitfalls
Not calling recordFlutterFatalError. Without this, widget build errors and layout overflows don't appear in Crashlytics. The FlutterError.onError assignment is mandatory.
Recording every error as fatal. Fatal errors count against your crash-free users rate. Only mark truly unrecoverable errors as fatal: true. Expected-but-logged errors should be fatal: false.
Not clearing the user identifier on sign-out. Call setUserIdentifier('') on sign-out to avoid cross-contaminating crash reports with the wrong user identity.
Sign in to like, dislike, or report.