Muhammad Uzair
Firebase

Flutter Firebase Crashlytics Setup in 10 Minutes

Published: 2026-10-11 · 7 min read

Set up Flutter Firebase Crashlytics in 10 minutes: real stack traces, custom keys, breadcrumbs, and non-fatal reports, plus the symbolication step teams skip.

Every app crashes. The difference between a 4.8-star app and a 3.9-star one is rarely the absence of crashes — it's how fast the team finds out about them. If your crash-reporting strategy is "users will leave a one-star review and tell us," you're flying blind. Setting up Flutter Firebase Crashlytics is the single highest-leverage thing you can do for app quality this week, and it genuinely takes about ten minutes once you know the exact steps.

What Crashlytics Actually Gives You

Firebase Crashlytics is a crash reporter, not a logging service. It captures fatal crashes and non-fatal errors from your Flutter app, groups identical crashes into "issues," and shows you the exact stack trace, device, OS version, and how many users are affected. The console even prioritizes issues by a "velocity" metric — crashes that are spiking right now float to the top so you fix the bleeding first.

Compared to rolling your own error reporting or relying on the Play Console's ANRs & crashes tab, Crashlytics gives you Flutter-specific stack traces, custom logs leading up to the crash (breadcrumbs), and the ability to attach custom keys so you know exactly which screen and user state triggered it. It's free at the scale most apps operate, and it integrates into the same Firebase project you probably already have for authentication or Firestore.

Step 1: Create the Firebase Project and Run FlutterFire CLI

If you already have a Firebase project with your app registered, skip ahead. Otherwise, create one in the Firebase console and register your Android and iOS apps with their package/bundle IDs.

The fastest reliable way to wire everything up is the FlutterFire CLI:

dart pub global activate flutterfire_cli
flutterfire configure

This interactive command reads your Firebase project, registers each platform app, downloads google-services.json (Android) and GoogleService-Info.plist (iOS), and writes lib/firebase_options.dart with the correct per-platform config. Accept the prompts and let it do its job — hand-copying config files is the source of half the Crashlytics setup bugs I've seen.

If flutterfire configure isn't an option (CI environments, monorepos), add the files manually and double-check the package names match your app exactly. A mismatched applicationId is the classic "Crashlytics shows no data" root cause.

Step 2: Add the Dependencies

Add these to your pubspec.yaml:

dependencies:
  firebase_core: ^x.y.z
  firebase_crashlytics: ^x.y.z

Then run flutter pub get. For Android, make sure your android/app/build.gradle applies the Google Services plugin (the FlutterFire docs cover this — apply plugin: 'com.google.gms.google-services') and that android/build.gradle includes the Google Services classpath. For iOS, GoogleService-Info.plist must be added to the Runner target in Xcode — dragging it into the Runner folder and confirming the target checkbox is the step people forget.

Step 3: Initialize Firebase in main.dart

Initialize Firebase before the app runs:

import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

Step 4: Wire Up the Error Handlers (The Part Everyone Skips)

This is where most setups silently fail. Adding the Crashlytics package does not automatically report Flutter framework errors or async errors — you have to route them there explicitly. There are three zones to cover:

import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // 1. Catch all Flutter framework errors
  FlutterError.onError = (errorDetails) {
    FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
  };

  // 2. Catch all uncaught async errors
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(const MyApp());
}

Note that in the handler above, recordFlutterFatalError is the correct API for FlutterError.onError — it marks the crash as fatal so it shows up correctly in the dashboard. The PlatformDispatcher.instance.onError handler covers errors in futures, streams, and timers that escape every try/catch.

One important detail: during development, you generally don't want Crashlytics uploading every hot-reload crash from your debug builds. The common pattern is to only enable collection in release/profile:

await FirebaseCrashlytics.instance
    .setCrashlyticsCollectionEnabled(!kDebugMode);

Or keep collection on in debug while testing (see Step 5), then gate it behind your build mode. Just be deliberate — silent debug crashes in the console are noise you'll learn to ignore, which trains you to ignore real ones too.

Step 5: Force a Test Crash and Verify

Add a temporary button somewhere in your debug build:

TextButton(
  onPressed: () => throw Exception('Test crash from Crashlytics setup'),
  child: const Text('Crash the app'),
),

Run a release or profile build (flutter run --release), tap the button, then relaunch the app — Crashlytics uploads the report on the next cold start, not at the moment of the crash. Open the Firebase console → Crashlytics. Your test crash should appear within a few minutes. If it doesn't, run through the checklist at the end of this article before touching anything else.

Going Further: Custom Keys, Logs, and Non-Fatal Errors

Fatal crashes are only half the value. The other half is context. When a crash report arrives with no idea which screen the user was on, you're still guessing.

Attach custom keys at key points in your app:

await FirebaseCrashlytics.instance.setCustomKey('screen', 'checkout');
await FirebaseCrashlytics.instance.setCustomKey('cart_items', 4);
await FirebaseCrashlytics.instance.setCustomKey('user_tier', 'premium');

Leave breadcrumb logs so the report shows what happened right before the crash:

await FirebaseCrashlytics.instance.log('User tapped pay button');
await FirebaseCrashlytics.instance.log('Payment API returned 402');

Identify the user (use your own user ID, never PII like email unless your privacy policy covers it):

await FirebaseCrashlytics.instance.setUserIdentifier('user_12345');

And record caught exceptions that didn't crash the app but still matter — a failed sync, a degraded feature:

try {
  await syncOfflineQueue();
} catch (e, stack) {
  FirebaseCrashlytics.instance.recordError(e, stack, fatal: false);
}

Non-fatal errors appear in a separate tab in the console. In my experience, they catch more real bugs per week than fatal crashes do, because the app "works" from the user's perspective while quietly failing in the background.

Release Builds: Symbolication (Android Mapping, iOS dSYMs)

If your crash stack traces show up as obfuscated garbage like a.b.c in release builds, symbolication is missing. For Android with R8/minification enabled, upload the mapping file — the Crashlytics Gradle plugin does this automatically when configured; the Firebase docs for "upload mapping files" walk through the exact Gradle snippet. For iOS, dSYMs need to reach Firebase: run the upload-symbols script as a build phase in Xcode, or upload them manually after each release build. This is the number-one reason teams say "Crashlytics works but the stack traces are useless."

Common Pitfalls Checklist

  • No data in the console: check the package/bundle ID matches the Firebase app registration exactly, and confirm you're looking at the right Firebase project.
  • Only debug crashes appear: collection is probably disabled in release, or you're testing on debug and never relaunching the app (upload happens on next cold start).
  • Obfuscated stack traces: mapping file (Android) or dSYMs (iOS) weren't uploaded for that build.
  • Flutter framework errors missing: FlutterError.onError was overridden somewhere else later in main() — make sure your Crashlytics assignment is the last one, or chain them.
  • Async errors missing: the PlatformDispatcher.instance.onError handler wasn't set, or a plugin swallowed the error before it reached the dispatcher.
  • Crashlytics tab shows "add SDK": the app hasn't successfully sent its first report yet — force a test crash in a release build and relaunch.

Ten minutes of setup buys you a permanent, always-on crash radar for your app. The teams that ship the most stable Flutter apps aren't the ones that write bug-free code — they're the ones that find out about bugs in minutes instead of in reviews. Set up Crashlytics today, force a test crash, and confirm the dashboard lights up before your next release.

Building a Flutter app and want crash reporting, analytics, and a release pipeline set up properly from day one? Get in touch — I've shipped this exact stack on production apps and can get yours instrumented in a single session.

Frequently Asked Questions

How do I report Flutter framework errors to Crashlytics?

Set FlutterError.onError to call recordFlutterFatalError, and set PlatformDispatcher.instance.onError to call FirebaseCrashlytics.instance.recordError with fatal true. The package alone does not report framework or async errors — both handlers must be wired explicitly, and the FlutterError assignment should be the last one in main().

Should Crashlytics be enabled in debug builds?

Gate collection with setCrashlyticsCollectionEnabled(!kDebugMode) so debug builds do not spam the console with hot-reload crashes. When testing, enable it in debug, force a test crash in a release or profile build, then relaunch — Crashlytics uploads reports on the next cold start, not at crash time.

Why are my Crashlytics stack traces obfuscated in release builds?

Obfuscated traces mean symbolication files are missing. For Android with R8/minification, the Crashlytics Gradle plugin uploads the mapping file automatically when configured; for iOS, run the upload-symbols script as a build phase in Xcode or upload dSYMs manually after each release build.

Related Articles

← Back to all articles