Client-Side Error Logging in Flutter Mobile Apps Stop Guessing, Start Using Firebase Crashlytics

Stop guessing what is wrong in your mobile apps. This guide shows how to see errors in your mobile app with Firebase Crashlytics, No more vague “Something went wrong”.
Table of contents

TL;DR  // summary

Backend errors are trivial. SSH in, check CloudWatch, Kibana, whatever. Done.

Client-side errors in a mobile app? That's where it gets painful. You don't ship full logs (bandwidth and cost would murder you), and 90%+ of production issues are non-fatal garbage that never crashes the app. Vague UI toasts like "Something went wrong" tell you nothing. You need stack traces, context, and metadata without turning your app into a data hose.

In this guide I will show you how to move from this vague UI side exception like this:

To a detailed error message with stack trace and metadata.

The Problem in Production

You don't publish every client-side log. That would cost a fortune and kill battery/network. Crash reports catch the big ones, but non-critical errors? They slip through and cause support tickets, bad reviews, and lost users.

Multiple approaches exist for client-side logging (Sentry, Bugsnag, custom logging to your own backend, etc.). They all have trade-offs on cost, privacy, and setup pain. Firebase Crashlytics is free for most use cases, integrates cleanly with Flutter, and gives you what you need without the enterprise pricing headache. That's what this guide covers end-to-end.

If you’re using an AI coding agent, install the Firebase agent skills first. It can automate the initial Firebase project creation, run flutterfire configure and handle most of the repetitive config steps for you. When you get stuck anywhere in the process, just ask the AI to help you troubleshoot.
claude plugin marketplace add firebase/agent-skills
claude plugin install firebase@firebase

Step 1: Create a Firebase Project

Privacy and Compliance Reality

This setup ships full stack traces and user IDs straight to Google. Fine for consumer apps. In banking, fintech, or any regulated environment that's a compliance violation waiting to happen. You need explicit consent, PII redaction on-device, data retention rules, and audit logs.

I've watched entire error-logging projects get killed in audit because someone just flipped the switch like this guide says. If you're handling real user money or personal data, treat this as the starting point, not the finish line. Most serious teams end up wrapping recordError with a sanitization layer.

Go to https://firebase.google.com/ and click Get started in console.

You'll need a Google account (use the same one as your Google Cloud if you're already there). Make sure you're using the right Google account!

Choose Get started by setting up a Firebase project.

Name it something clear (e.g., myapp-prod).

Create the project.

If you have multiple mobile apps: Create a separate Firebase project for each one. Mixing them turns your Crashlytics dashboard into an unreadable mess.

Enable Google Analytics when prompted, the extra breadcrumb data makes errors actually understandable.

Step 2: Add Your Apps to the Firebase Project

Press on Add app.

Firebase gives us 5 platforms including Flutter. Firebase talks to the native Android and iOS layers. The "Flutter" option in the console is just a tutorial helper, it doesn't register the real platform apps you need for production Crashlytics and Analytics.

Register both an Android app and an iOS app in the same Firebase project.

You should register your app for each environment. We don't want to test this set up in production. I highly recommend to follow the steps below for your DEV environment first and then add production configuration after you've successfully tested the setup.

Android Setup

In the Firebase console, click the Android icon.

Find your Android package name in android/app/build.gradle.kts under applicationId (or android/app/src/main/AndroidManifest.xml).

Enter the package name, and optionally nickname and debug SHA-1 (not required for Crashlytics).

Download google-services.json and drop it into android/app/.

Skip the "Add Firebase SDK" section in the console, we'll handle that properly below.

iOS Setup

In the console, click the iOS icon.

Enter your Apple Bundle ID (same as in Xcode → Runner target → General → Bundle Identifier). It must match exactly.

Optionally add App Name and App Store ID (only if your app is live or in TestFlight).

Download GoogleService-Info.plist and place it in ios/Runner/.

In Xcode:

  • Open ios/Runner.xcworkspace
  • Right click on Runner folder and select "Add Files to "Runner"..
  • Select downloaded GoogleService-Info.plist
  • Select Runner target → Build Phases
  • Only select the Runner target when adding the plist
  • Finish

Skip steps 3, 4, and 5 in the console wizard.

Step 3: iOS Native Setup (Xcode)

Open Xcode (Runner.xcworkspace).

Go to Runner target → Build Phases.

Click the + button → New Run Script Phase.

Drag it so it sits below "Embed Frameworks".

Name it "Firebase Crashlytics" (double-click the phase name).

Paste this exact script:

${PODS_ROOT}/FirebaseCrashlytics/run

(If using CocoaPods with the newer Firebase SDK layout, the official script is now sometimes "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run" — check your Pods folder if the first one fails.)

Verify Debug Symbols (Critical)

Targets → Runner → Build Settings → search "Debug Information Format".

Make sure the top row (Runner, all configurations, especially Release) is set to DWARF with dSYM File.

Without this, your iOS stack traces are garbage in Crashlytics.

Run cd ios && pod install (or flutter build ios which does it).

Step 4: Flutter Project Setup

First, add the dependencies (2026 versions):

flutter pub add firebase_core firebase_crashlytics firebase_analytics

Strongly recommended modern way:

Run flutterfire configure from your project root. It handles config files, Gradle plugins, and options automatically. Do this after creating the Firebase project and registering platforms.

If doing everything manually (legacy projects or you like pain):

Update Gradle (Android)

In android/build.gradle.kts (project-level), inside the allprojects or buildscript block (newer AGP uses plugins block but keep compatibility):

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath("com.google.gms:google-services:4.4.4")  // latest 2026
        classpath("com.google.firebase:firebase-crashlytics-gradle:3.0.6")  // latest 2026
    }
}

In android/app/build.gradle.kts:

plugins {
    // ... your existing plugins (id("com.android.application"), kotlin, etc.)
    id("com.google.gms.google-services")
    id("com.google.firebase.crashlytics")
}

Rebuild: flutter clean && flutter pub get && flutter build apk (or whatever).

Step 5: Initialize in main.dart

Put this at the very start of your app (right after WidgetsFlutterBinding.ensureInitialized()):

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 'dart:ui';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp();

  // Only enable in release (or use your own env flag)
  await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(kReleaseMode);

  // Flutter framework errors
  FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

  // Async / isolate / uncaught errors
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(const MyApp());
}

For a cleaner setup, wrap it in a class exactly like the draft wanted:

class FirebaseConfig {
  static Future<void> setUpFirebaseCrashAnalytics() async {
    await Firebase.initializeApp();

    await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(kReleaseMode);

    FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

    PlatformDispatcher.instance.onError = (error, stack) {
      FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
      return true;
    };
  }
}

Then call it in main:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await FirebaseConfig.setUpFirebaseCrashAnalytics();
  // ... rest of your app
}

Step 6: Record Non-Fatal Errors in Your Code

Catch blocks are where the magic happens:

try {
  // your risky code
  String? nullString = null;
  nullString!.toLowerCase();  // boom
} catch (error, stackTrace) {
  await FirebaseCrashlytics.instance.recordError(
    error,
    stackTrace,
    reason: 'Google Sign-In failed',  // context
    fatal: false,
  );
}

Step 7: Add Metadata and User Context

Anywhere in your app:

final crashlytics = FirebaseCrashlytics.instance;

crashlytics.setCustomKey('env', 'prod');
crashlytics.setCustomKey('userId', currentUser?.id ?? 'anonymous');
crashlytics.setUserIdentifier(currentUser?.id ?? '');  // shows up as User ID in console

Step 8: Testing It

Important: By default, Crashlytics only sends data from release builds. For debug testing, temporarily add:

await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);

Force a test error (null check, throw Exception, etc.).

Events are buffered on device and uploaded in batches when the app goes to background, restarts, or after a delay. This is intentional — it saves battery and network. Re-open the app a couple times if it doesn't appear instantly.

Step 9: View in Firebase Console

Go to your project → Crashlytics (under Run or left menu).

Filter out "Crashes" to see non-fatal errors.

You'll see:

  • Full stack trace
  • App version
  • Device/OS details
  • Custom keys (env, userId, etc.)
  • Breadcrumbs from Analytics

Tada — no more guessing.

Common Gotchas That Will Bite You

  • "Could not get GOOGLE_APP_ID" in Xcode: Plist not added to Runner target, wrong location, or didn't run pod install. Clean, pod install, rebuild.
  • Unreadable stack traces: Missing dSYM (iOS) or ProGuard/R8 mapping (Android obfuscated builds).
  • Nothing appears in console: Collection not enabled, no internet, or you didn't re-open the app after the error.
  • Simulator iOS: total waste of time.

This exact setup has run in production apps I've shipped. It catches the errors that actually matter without the overhead. Follow it step-by-step and you'll stop guessing why users see blank screens.

Ship it. Fix real bugs instead of playing detective.

ABOUT AUTHOR

[ NAME ].....................Mike Nova
[ BACKGROUND ].......Software Engineer @ Top-Tier Banks
[ BIO ]..........................10 years at top banks building massive financial systems that generate billions in revenue. This blog documents the reality in tech and top banks without the PR bs. I focus on tech that gets the job done and write articles that expose corporate theater.
[ CONNECT ]................
Substack
|
Twitter (X)
[NAME ]
Mike Nova
[ BACKGROUND ]
Software Engineer @ Top-Tier Banking
[ BIO ]
I’ve spent the last 8 years maintaining legacy monoliths, migrating them to the cloud, and watching Agile coaches burn millions of dollars. This blog is where I document the reality of the tech industry without the LinkedIn PR filters. I write code that moves money, and articles that expose the fluff.
[ CONNECT ]
Substack
|
Twitter (X)