Firebase Crashlytics — Error Reporting & Diagnostics
Firebase Crashlytics — Error Reporting & Diagnostics
Production apps crash. The difference between a professional app and an amateur one is how quickly you find out, how much context you have, and how fast you fix it. Firebase Crashlytics is a lightweight, real-time crash reporter that groups issues by root cause, highlights the most impactful ones, and gives you the breadcrumb trail — custom keys, log messages, and stack traces — needed to reproduce and resolve them.
Adding the Dependency
Add firebase_crashlytics to pubspec.yaml alongside the core Firebase package:
# pubspec.yaml
dependencies:
firebase_core: ^3.6.0
firebase_crashlytics: ^4.1.3
Run flutter pub get and ensure Firebase.initializeApp() is called in main() before any other Firebase usage.
google-services.json file and the Google Services Gradle plugin. On iOS it requires GoogleService-Info.plist. Both are downloaded from the Firebase console when you register your app.Wiring Up Fatal & Non-Fatal Error Handlers
The most important step is routing all unhandled exceptions — from Flutter, from the Dart isolate, and from platform threads — into Crashlytics. Do this once in main():
import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// Pass all uncaught Flutter framework errors to Crashlytics.
FlutterError.onError =
FirebaseCrashlytics.instance.recordFlutterFatalError;
// Pass all uncaught asynchronous errors from the root isolate
// (e.g. errors thrown inside runApp or Future chains).
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true; // returning true suppresses the default error handler
};
// Wrap runApp in a guarded zone so synchronous errors are also caught.
runApp(const MyApp());
}
FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(!kDebugMode) right after initializeApp.Recording Non-Fatal Errors
Not every error crashes the app. Network timeouts, failed JSON parses, and handled exceptions are still worth tracking as non-fatal events. Call recordError with fatal: false (the default) inside your catch blocks:
Future<void> loadUserProfile(String uid) async {
try {
final doc = await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.get();
if (!doc.exists) throw Exception('User $uid not found');
_profile = UserProfile.fromMap(doc.data()!);
} catch (e, stack) {
// Non-fatal: the UI can show a fallback, but we still want a report.
await FirebaseCrashlytics.instance.recordError(
e,
stack,
reason: 'loadUserProfile failed for uid=$uid',
fatal: false,
);
_profile = UserProfile.empty();
}
}
Custom Keys & Log Messages
Raw stack traces often lack the context needed to reproduce a crash. Crashlytics lets you attach up to 64 custom key–value pairs and write a circular log buffer (last 64 KB) that is included with every report:
// Attach key–value metadata (updated as app state changes).
await FirebaseCrashlytics.instance.setCustomKey('user_role', 'premium');
await FirebaseCrashlytics.instance.setCustomKey('subscription_tier', 3);
await FirebaseCrashlytics.instance.setCustomKey('locale', 'ar_SA');
// Write timestamped breadcrumbs to the log buffer.
FirebaseCrashlytics.instance.log('Navigated to checkout screen');
FirebaseCrashlytics.instance.log('Applied coupon: SAVE20');
FirebaseCrashlytics.instance.log('Payment provider: Stripe');
// Optionally identify the user (use an opaque ID, never PII).
await FirebaseCrashlytics.instance.setUserIdentifier('user_8f3a91b');
Triggering a Test Crash
After wiring everything up, send a test crash to verify the pipeline before shipping:
// Add a temporary button during development ONLY.
// Remove before releasing to production.
ElevatedButton(
onPressed: () => FirebaseCrashlytics.instance.crash(),
child: const Text('Test Crash'),
),
Force-kill and relaunch the app. Within a few minutes the crash appears in the Firebase console under Crashlytics > Issues.
Reading Reports in the Firebase Console
Every Crashlytics issue page shows:
- Stack trace — deobfuscated (if you upload dSYMs / ProGuard mappings)
- Custom keys — the snapshot of key–value pairs at crash time
- Logs — the last 64 KB of breadcrumbs leading to the crash
- Device & OS info — model, OS version, RAM, orientation
- Session metadata — app version, foreground/background, crash-free users %
Summary
Firebase Crashlytics turns opaque crashes into actionable diagnostics. The integration checklist is: add the package, call initializeApp, hook FlutterError.onError and PlatformDispatcher.instance.onError to recordFlutterFatalError / recordError, enrich reports with custom keys and log breadcrumbs, and monitor the Firebase console. With this in place your team is notified of regressions within minutes, not days.