← Articles

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:

  1. Re-open the app (crashes are sent on next launch)
  2. Wait ~1 minute for Firebase to process
  3. Check Crashlytics dashboard

AlertPolicies for crash rate

Set up Firebase Performance Monitoring or Crashlytics email alerts:

  1. Firebase Console → Crashlytics → Email alerts
  2. 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.

Crash reporting in Flutter with Firebase Crashlytics — ANN Tech