---
title: "Crash Reporting in a Capacitor App with Crashlytics"
description: Add crash reporting to a Capacitor app with Firebase Crashlytics, from console setup and native config to non-fatal errors, custom keys, and testing.
date:
  created: 2026-08-29
  updated: 2026-08-29
authors:
  - djabif
categories:
  - Capacitor
  - Firebase
  - Guides
  - SDKs
links:
  - Capacitor Firebase Crashlytics: sdks/capacitor/firebase/crashlytics.md
faq: true
---

# Crash Reporting in a Capacitor App with Crashlytics

The crashes that hurt most are the ones you never hear about — the user hits a bug, force-quits, and leaves a one-star review instead of a report. Crash reporting closes that gap by capturing the stack trace, the device, and the steps leading up to a crash automatically. The [Capacitor Firebase Crashlytics plugin](../../sdks/capacitor/firebase/crashlytics.md) wires your app into [Firebase Crashlytics](https://firebase.google.com/docs/crashlytics/){:target="_blank"} using the native Android and iOS SDKs, so those reports land in your Firebase console without you building a reporting pipeline.

This guide covers the whole path: enabling Crashlytics in the console, the native setup that catches most teams out (the Gradle plugin on Android, dSYM upload on iOS), recording fatal and non-fatal errors from your web layer, and verifying it all works on a device.

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="/" target="_blank">
    <img alt="Build and deploy your Capacitor app with Capawesome Cloud" src="https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1" />
  </a>
</div>

## Key Takeaways

- Crashlytics crash reporting is **Android and iOS only** — there's no web implementation.
- Setup needs the **Crashlytics Gradle plugin** on Android and **dSYM upload** on iOS, on top of the usual Firebase config files.
- Native crashes are captured automatically; log handled errors yourself with [`recordException(...)`](../../sdks/capacitor/firebase/crashlytics.md#recordexception), paired with [stacktrace.js](https://www.stacktracejs.com/){:target="_blank"} for readable JavaScript stack traces.
- `setCustomKey(...)`, `setUserId(...)`, and `log(...)` attach context that makes crashes reproducible.
- `setEnabled(...)` toggles automatic collection (it applies on the next app run) — the hook for consent flows.
- Force a test crash with `crash(...)` to confirm reports reach the console.

## How to Add Crash Reporting to a Capacitor App

Adding Crashlytics crash reporting to a Capacitor app takes four steps:

1. **Enable Crashlytics** in your Firebase project.
2. **Install** `@capacitor-firebase/crashlytics` and sync the native projects.
3. **Add the native config** — the Firebase config files, the Crashlytics Gradle plugin on Android, and dSYM upload on iOS.
4. **Record errors** — native crashes are automatic; log handled ones with `recordException()`, and add context with custom keys, a user ID, and log messages.

Each step gets its own section below, along with a global error handler, best practices, and troubleshooting.

## What Is Firebase Crashlytics?

[Firebase Crashlytics](https://firebase.google.com/docs/crashlytics/){:target="_blank"} is Google's crash reporting service. It collects crashes and handled errors from your app, groups them into issues, and reports them in the Firebase console with stack traces, device breakdowns, and the logs leading up to each event. The [Capacitor Firebase Crashlytics plugin](../../sdks/capacitor/firebase/crashlytics.md) exposes the native Android and iOS Crashlytics SDKs behind one shared TypeScript API.

One thing to know up front: **this plugin is Android and iOS only.** Crashlytics has no web SDK, so calls have no effect in the browser. That's usually fine — the crashes worth chasing are the native ones on real devices — but it means crash reporting is one of the few Firebase plugins you can't lean on for a PWA.

A working example can be found here: [capawesome-team/capacitor-firebase-plugin-demo](https://github.com/capawesome-team/capacitor-firebase-plugin-demo){:target="_blank"}.

## Why Use Crashlytics in a Capacitor App?

Four scenarios cover most of what teams use the plugin for:

- **Crash reporting.** Collect native crash reports from Android and iOS automatically and inspect them in the Firebase console.
- **Non-fatal error tracking.** Record handled exceptions with [`recordException(...)`](../../sdks/capacitor/firebase/crashlytics.md#recordexception), including JavaScript stack traces from your web layer.
- **Debugging context.** Attach custom keys, log messages, and a user ID so a crash comes with the state that caused it, not just a stack trace.
- **Privacy compliance.** Turn automatic collection on or off at runtime, for example only after the user has consented.

## Before You Start

You'll need a Capacitor app with at least one native platform (`android` or `ios`) and a Firebase project — the [Firebase console](https://console.firebase.google.com/){:target="_blank"} will set one up in a minute if you don't have one yet. Since Crashlytics is native-only, there's no web platform to configure.

## Step 1: Enable Crashlytics in the Firebase Console

1. In the [Firebase console](https://console.firebase.google.com/){:target="_blank"}, open **Run → Crashlytics** and click **Enable Crashlytics**.
2. The console will wait for its first report from your app. You'll send one at the end of this guide by forcing a test crash — until then, the dashboard shows a "waiting for data" state.

## Step 2: Install the Plugin

Install the plugin and sync the native projects:

```bash
npm install @capacitor-firebase/crashlytics
npx cap sync
```

If you plan to send JavaScript stack traces with your non-fatal reports, also install [stacktrace.js](https://www.stacktracejs.com/){:target="_blank"}:

```bash
npm install stacktrace-js
```

## Step 3: Add Firebase and the Native Crashlytics Setup

This is the step most Crashlytics problems trace back to. On top of the usual Firebase config files, Crashlytics needs a build-time integration on each platform.

**Add Firebase to your native apps** ([full reference](https://github.com/capawesome-team/capacitor-firebase/blob/main/docs/firebase-setup.md){:target="_blank"}): register an Android app and drop `google-services.json` into `android/app/`, and register an iOS app and add `GoogleService-Info.plist` to `ios/App/App/` (drag it into the Xcode project, all targets).

**Android — the Crashlytics Gradle plugin.** Add the plugin's classpath to your project-level `android/build.gradle`:

```groovy
buildscript {
  dependencies {
    classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9'
  }
}
```

Then apply it in your app-level `android/app/build.gradle`:

```groovy
apply plugin: 'com.google.firebase.crashlytics'
```

**iOS — Swift Package Manager and dSYM upload.** If you use SPM, set the `symlink` package option in `capacitor.config.ts` so the Firebase packages don't collide on SwiftPM package identity (requires Capacitor CLI 8.4.0+):

```json
{
  "experimental": {
    "ios": {
      "spm": {
        "packageOptions": {
          "@capacitor-firebase/crashlytics": { "symlink": true }
        }
      }
    }
  }
}
```

For readable iOS crash reports, Crashlytics needs your build's **debug symbol (dSYM) files**. In Xcode, select your app target, and under **Build Settings** set **Debug Information Format** to `DWARF with dSYM File` for all build types. Then add a **New Run Script Phase** as your target's *last* build phase that runs the Crashlytics upload script — see the plugin's [Installation](../../sdks/capacitor/firebase/crashlytics.md#installation) reference for the exact script. Skip this and your iOS crashes arrive as unreadable memory addresses instead of symbolicated stack traces.

## Recording Crashes and Non-Fatal Errors

Native crashes are captured automatically once the setup above is done — you don't call anything to report a real crash. What you *do* call is `recordException(...)` for errors you catch and handle but still want to know about, and `crash(...)` to verify the pipeline.

Record a handled error as a non-fatal report with [`recordException(...)`](../../sdks/capacitor/firebase/crashlytics.md#recordexception). Because the SDK is native, a plain JavaScript error won't carry a useful stack trace across the bridge on its own — generate one with [stacktrace.js](https://www.stacktracejs.com/){:target="_blank"} and pass it along:

```typescript
import { FirebaseCrashlytics } from '@capacitor-firebase/crashlytics';
import * as StackTrace from 'stacktrace-js';

const recordError = async (error: Error) => {
  const stacktrace = await StackTrace.fromError(error);
  await FirebaseCrashlytics.recordException({
    message: error.message,
    stacktrace,
  });
};
```

To confirm reporting works end to end, force a crash with [`crash(...)`](../../sdks/capacitor/firebase/crashlytics.md#crash) — remove this before shipping:

```typescript
import { FirebaseCrashlytics } from '@capacitor-firebase/crashlytics';

const forceCrash = async () => {
  await FirebaseCrashlytics.crash({ message: 'Test crash' });
};
```

## Native Crashes vs. Your JavaScript Errors

This is the part most teams discover the hard way. In a Capacitor app, most of your code runs as JavaScript inside a WebView, and Crashlytics' automatic capture only catches **native** crashes — the kind that close the whole app. Those are relatively rare in a hybrid app. Far more often, a bug throws a JavaScript error that breaks a screen without crashing the process, and Crashlytics never sees it.

That's why the global error handler below isn't optional for hybrid apps — it's the main way your own bugs reach Crashlytics at all. And there's a second catch worth knowing: Crashlytics symbolicates *native* code (via dSYM on iOS and the R8 mapping file on Android, which the Gradle plugin uploads for you), but it does **not** process your JavaScript source maps. A raw JS stack trace points into your minified bundle, not your source. That's exactly what [stacktrace.js](https://www.stacktracejs.com/){:target="_blank"} solves: it resolves the error against your source maps in the browser, so the frames you attach to `recordException(...)` point at your real TypeScript.

The takeaway: pair a global error handler with `recordException(...)` and stacktrace.js, or Crashlytics will report the occasional native crash while staying blind to the JavaScript bugs that actually affect your users.

## Adding Context to Your Reports

A stack trace tells you where a crash happened; custom keys, a user ID, and logs tell you *why*. These attach to every subsequent fatal and non-fatal report:

```typescript
import { FirebaseCrashlytics } from '@capacitor-firebase/crashlytics';

const addContext = async () => {
  await FirebaseCrashlytics.setCustomKey({ key: 'screen', value: 'checkout', type: 'string' });
  await FirebaseCrashlytics.log({ message: 'Tapped "Place order"' });
};
```

Set a user ID with [`setUserId(...)`](../../sdks/capacitor/firebase/crashlytics.md#setuserid) so you can trace a crash back to a specific account. Use a stable, non-personal identifier — the Firebase user ID from the [Capacitor Firebase Authentication guide](./capacitor-firebase-authentication-guide.md) is a good fit, not an email address:

```typescript
import { FirebaseCrashlytics } from '@capacitor-firebase/crashlytics';

const setUserId = async (userId: string) => {
  await FirebaseCrashlytics.setUserId({ userId });
};
```

## Catching Uncaught Errors in Angular, React, or Vue

The most valuable place to call `recordException(...)` is your framework's global error handler, so any unhandled error in your web layer becomes a non-fatal report instead of vanishing into the console. Each framework has its own hook:

=== "Angular"

    ```typescript
    import { ErrorHandler, Injectable } from '@angular/core';
    import { FirebaseCrashlytics } from '@capacitor-firebase/crashlytics';
    import * as StackTrace from 'stacktrace-js';

    @Injectable()
    export class CrashlyticsErrorHandler implements ErrorHandler {
      async handleError(error: Error): Promise<void> {
        const stacktrace = await StackTrace.fromError(error);
        await FirebaseCrashlytics.recordException({ message: error.message, stacktrace });
        console.error(error);
      }
    }
    // Register: providers: [{ provide: ErrorHandler, useClass: CrashlyticsErrorHandler }]
    ```

=== "React"

    ```tsx
    import { Component, ErrorInfo, ReactNode } from 'react';
    import { FirebaseCrashlytics } from '@capacitor-firebase/crashlytics';
    import * as StackTrace from 'stacktrace-js';

    export class CrashlyticsBoundary extends Component<{ children: ReactNode }> {
      async componentDidCatch(error: Error, _info: ErrorInfo) {
        const stacktrace = await StackTrace.fromError(error);
        await FirebaseCrashlytics.recordException({ message: error.message, stacktrace });
      }
      render() {
        return this.props.children;
      }
    }
    ```

=== "Vue"

    ```typescript
    import { createApp } from 'vue';
    import { FirebaseCrashlytics } from '@capacitor-firebase/crashlytics';
    import * as StackTrace from 'stacktrace-js';
    import App from './App.vue';

    const app = createApp(App);
    app.config.errorHandler = async err => {
      const error = err as Error;
      const stacktrace = await StackTrace.fromError(error);
      await FirebaseCrashlytics.recordException({ message: error.message, stacktrace });
    };
    ```

## Controlling Data Collection

Crashlytics collects data automatically by default. If you need consent first (GDPR and similar), disable automatic collection and only enable it once the user agrees. Note that [`setEnabled(...)`](../../sdks/capacitor/firebase/crashlytics.md#setenabled) applies on the *next* app run, not immediately:

```typescript
import { FirebaseCrashlytics } from '@capacitor-firebase/crashlytics';

const setCollectionEnabled = async (enabled: boolean) => {
  await FirebaseCrashlytics.setEnabled({ enabled });
};
```

With automatic collection off, reports gathered during a session are held on the device. Decide their fate on the next launch with [`sendUnsentReports()`](../../sdks/capacitor/firebase/crashlytics.md#sendunsentreports) or [`deleteUnsentReports()`](../../sdks/capacitor/firebase/crashlytics.md#deleteunsentreports). You can also check [`didCrashOnPreviousExecution()`](../../sdks/capacitor/firebase/crashlytics.md#didcrashonpreviousexecution) at startup to, say, show a "sorry, something went wrong" prompt.

## Run on a Device and Verify

Crashlytics only reports from a real device or emulator, so build and launch the native app:

```bash
npx cap sync
npx cap run android   # or: npx cap run ios
```

Then confirm the pipeline works:

1. Call `crash(...)` from your UI to force a crash. The app closes — this is expected.
2. **Relaunch the app.** Crashlytics uploads the pending report on the next launch, not at the moment of the crash.
3. Open **Crashlytics** in the Firebase console. The test crash should appear within a few minutes.

If nothing shows up, it's almost always the native build integration (the Gradle plugin on Android, or a missing dSYM upload on iOS) — see the troubleshooting section below.

## Crashlytics Best Practices

### Send a JavaScript Stack Trace With Every Non-Fatal

Because the SDK is native, a `recordException(...)` call without a `stacktrace` gives you the message but not where it came from in your web code. Always generate one with [stacktrace.js](https://www.stacktracejs.com/){:target="_blank"} so the report points at the actual line in your TypeScript, not the bridge.

### Don't Log Personal Data

Custom keys, logs, and the user ID all end up in your crash reports. Keep them free of emails, names, and tokens — use a stable internal ID for `setUserId(...)` and describe state ("cart has 3 items"), not the data itself.

### Leave a Test Crash Out of Production

`crash(...)` exists to verify your setup, nothing else. Guard it behind a dev-only flag or remove it before you ship, so a stray call can't take down a real user's session.

### Wire Up the Global Error Handler Early

Registering the framework error handler (above) at app startup means you capture errors from the very first screen. Wiring it up late leaves a blind spot exactly where early-lifecycle bugs tend to hide.

## Common Errors and Troubleshooting

- **iOS crashes show up as memory addresses, not code.** The dSYM files aren't reaching Crashlytics. Set **Debug Information Format** to `DWARF with dSYM File` and confirm the upload Run Script phase is your target's last build phase (Step 3).
- **No crashes appear on Android.** The Crashlytics Gradle plugin isn't applied. Check that the `firebase-crashlytics-gradle` classpath is in the project-level Gradle file and `apply plugin: 'com.google.firebase.crashlytics'` is in the app-level one.
- **The test crash never arrives.** Two usual causes: Crashlytics won't upload while a **debugger is attached** (run the app without Xcode/Android Studio driving it), and reports upload on the *next* launch, not at crash time — so relaunch the app. If it still doesn't show, the `google-services.json` / `GoogleService-Info.plist` file is missing or misplaced (Step 3).
- **`isEnabled()` throws or returns nothing.** It's **iOS only**. On Android, track the collection state yourself.
- **Nothing works in the browser.** Crashlytics has no web implementation — test on a device or emulator, not the dev server.
- **Non-fatals have no useful stack trace.** You're calling `recordException(...)` without a `stacktrace`. Generate one with stacktrace.js and pass it along.

## FAQ

### Does Firebase Crashlytics work on the web with Capacitor?

No. The Capacitor Firebase Crashlytics plugin is Android and iOS only — there's no web SDK, so calls are no-ops in the browser. Test crash reporting on a real device or emulator. If you need error tracking on the web too, pair it with a separate web error-reporting service.

### How do I report handled (non-fatal) errors, not just crashes?

Call [`recordException(...)`](../../sdks/capacitor/firebase/crashlytics.md#recordexception) from your `catch` blocks or a global error handler, passing the error message and a stack trace generated by [stacktrace.js](https://www.stacktracejs.com/){:target="_blank"}. These appear in the console as non-fatal issues, separate from crashes.

### Why are my iOS crash reports unreadable?

Crashlytics needs your build's dSYM files to turn memory addresses into symbolicated stack traces. Set the debug information format to `DWARF with dSYM File` and make sure the Crashlytics upload script runs as the last build phase, as described in Step 3.

### Why don't crashes show up when I run the app from Xcode or Android Studio?

Crashlytics doesn't upload reports while a debugger is attached, and it uploads on the *next* app launch regardless. To test, run the app without the debugger driving it, trigger the crash, then relaunch. Waiting for a report while still attached to the debugger is the single most common reason a test crash seems to "not work."

### Does Crashlytics capture app freezes (ANRs) on Android?

Yes. Alongside crashes and non-fatal exceptions, Crashlytics reports Application Not Responding (ANR) events on Android — the freezes where the UI thread is blocked long enough for the system to offer to close the app. They show up as their own issue type in the console, separate from crashes.

### How long does it take for a crash to appear in the Firebase console?

A report uploads on the next app launch after the crash, then usually appears in the console within a few minutes. The very first event for a newly enabled app can take a little longer while Crashlytics sets up the dashboard, so don't panic if your first test crash lags.

### Is Firebase Crashlytics free to use?

Yes. Crashlytics has no usage-based billing — it's free regardless of how many reports your app sends, which makes it one of the Firebase products with no cost consideration when deciding whether to adopt it. Check the current [Firebase pricing page](https://firebase.google.com/pricing){:target="_blank"} for the latest details.

## Turn a Crash Report Into a Shipped Fix the Same Day

A crash report is only useful if you can act on it fast, and because your app's logic runs in the web layer, most crash fixes are web-layer changes — which don't have to wait on an app store review. [Capawesome Cloud](https://capawesome.io/){:target="_blank"} builds your iOS and Android apps in the cloud and pushes those fixes straight to users with live updates, so you can go from a Crashlytics report to a shipped fix the same day, and automate App Store and Play Store submission when a native release is needed.

[Try Capawesome Cloud Free](https://capawesome.io){ .md-button .md-button--primary }

## Conclusion

Crashlytics turns silent crashes into actionable reports, and the plugin makes that a native integration rather than a pipeline you build. The work is mostly in the setup — the Gradle plugin on Android and dSYM upload on iOS — after which native crashes report themselves and you only reach for the API to log non-fatals and attach context. Remember the two constraints that trip people up: it's Android and iOS only, and iOS reports are unreadable without dSYM upload.

If you want to go deeper from here:

- [Firebase Authentication in Capacitor: Setup & Best Practices](./capacitor-firebase-authentication-guide.md) — get the stable user ID to attach to your crash reports with `setUserId(...)`.
- [Track App Events with Firebase Analytics in Capacitor](./capacitor-firebase-analytics-guide.md) — the sibling Firebase plugin for measuring behavior, often paired with Crashlytics for stability.
- [Capacitor Firestore: Real-Time Data & Offline Sync](./capacitor-firebase-cloud-firestore-guide.md) — database errors are among the most common non-fatals you'll end up recording.

Ran into a crash report that doesn't add up, or a setup issue this guide didn't cover? Ask in the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"} — and the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"} delivers the next guide as soon as it's out.
