Expo apps crash on production devices, and the only way to know it happened is through real-time error notifications with source maps and crash analytics. This guide covers Expo error tracking React Native production — how to instrument your Expo app to catch errors instantly, compare Sentry and Firebase Crashlytics, and understand why one solution beats the other for mobile teams. By the end, every uncaught exception in your app will land on your dashboard grouped, contextualized, and ready to fix — not buried in buried in silence.
Error tracking isn't optional for production Expo apps. Without it, users hit bugs in the wild, don't report them, and churn silently. Your team has no visibility into crashes, freezes, or performance regressions. Real-time error tracking solves this: SDK-based instrumentation hooks into your app's native runtime, captures every uncaught exception before the app crashes, and streams them to your dashboard where you can triage and fix them faster than your users can complain.
Why Expo apps need error tracking
Expo simplifies React Native development by handling the native build layer — you write JavaScript and Expo Prebuild generates Android and iOS projects. But running on real devices means real problems: network timeouts, out-of-memory crashes, native module failures, and race conditions that only show up at scale. Your development environment doesn't surface these, and user feedback is slow.
Error tracking adds visibility. Without it, you're shipping blind. With it, a crash that hits one thousand users alerts you in seconds. You see the stack trace (de-symbolicated for native code), affected user count, device models, OS versions, and the exact breadcrumbs — network calls, state changes, taps — that led to the crash. This transforms debugging from a support ticket guessing game into precision work.
Sentry vs Firebase Crashlytics: a mobile-first comparison
Both Sentry and Firebase Crashlytics are industry-standard solutions for mobile error tracking. They both capture crashes, provide stack traces, and offer dashboards. But for Expo, the choice matters.
Firebase Crashlytics is tightly integrated with Google Play and Apple App Store. If you already use Firebase (for auth, database, analytics), Crashlytics feels native — no separate account, no new dashboard. Crashlytics is free up to unlimited events, making the pricing question disappear. On Android, Crashlytics captures native crashes in C/C++ code, which is powerful if you're using native modules heavily.
The downside: Firebase Crashlytics is primarily a crash reporter, not an error tracker. It focuses on unhandled exceptions and native crashes. If you want to debug production errors more broadly — API failures, slow frames, network issues, handled exceptions you deliberately log — Crashlytics forces you to use Firebase Remote Config and custom logging, which feels bolted-on. It also offers limited visibility into service performance, so if your Expo app calls a backend and something's slow, you're flying blind on the server side.
Sentry is a purpose-built error tracker that treats mobile crashes and errors the same way it treats web or backend errors. With Sentry's React Native SDK, your Expo app sends errors, breadcrumbs, and performance data to Sentry's dashboard. Sentry groups crashes automatically, offers release tracking to know which deploy introduced a regression, and integrates with GitHub to link stack frames directly to your source code. If you're already using Sentry for your backend (Node.js API, Python workers, etc.), unified error tracking across all your services is powerful — one dashboard, one alert rule system, consistent grouping.
Sentry's pricing is transparent: free tier (5k events/month), Team ($26/month for 50k events), or Business. For a mobile app sending crashes and errors, 50k events per month is generous — you'd need a very large user base to exceed it. But if you're cost-sensitive or shipping a passion project, the free tier's 5k limit might cramp you.
LightTrace is a Sentry-SDK-compatible alternative that's faster and cheaper. Pricing is transparent: free tier (5k events/month), Team ($29/month for 250k events), or Business. For mobile apps, LightTrace's Team plan at $29/month handles 250k events — five times Sentry's Team plan for $3 more per month. That's a straightforward value proposition. LightTrace is hosted and maintained, so there's no operational burden. If you want error tracking without the Sentry enterprise overhead, LightTrace is worth testing.
Firebase Crashlytics is best if you're all-in on Google Play / App Store integration and don't need distributed tracing. Sentry is best if you have a backend and want unified error tracking across mobile and server. LightTrace is best if you want Sentry's SDK compatibility and lower cost.
Setting up error tracking in your Expo app
The pattern is identical across all error trackers: install the SDK, initialize it at startup with a DSN (endpoint), and errors flow automatically. For Expo with Sentry or LightTrace, here's the quickstart.
Install the Sentry React Native SDK:
npm install @sentry/react-native
npx sentry-wizard --integration reactNativeExpo
The wizard handles the setup — it modifies your app.json, installs native dependencies, and generates an initial config.
Initialize at app startup:
import * as Sentry from "@sentry/react-native";
Sentry.init({
dsn: "https://<key>@light-trace.robomiri.com/1",
environment: "production",
release: "com.example.myapp@1.0.0",
tracesSampleRate: 1.0,
integrations: [
new Sentry.ReactNativeTracing(),
],
});
export default function App() {
return <RootNavigator />;
}
That's it. From now on, every uncaught exception in your app — in JavaScript, native code, or during navigation — is captured and sent to your error tracker.
For Firebase Crashlytics:
npm install @react-native-firebase/app @react-native-firebase/crashlytics
Then initialize in your app entry:
import crashlytics from "@react-native-firebase/crashlytics";
import { useEffect } from "react";
useEffect(() => {
// Automatically log uncaught exceptions
crashlytics().setCrashlyticsCollectionEnabled(true);
}, []);
Firebase Crashlytics works out of the box with less boilerplate, but if you want to add custom error handling or integrate with your backend's error data, Sentry's ecosystem is deeper.
The DSN (e.g., https://<key>@light-trace.robomiri.com/1) identifies your project and environment, not any secret. It's safe to commit to your repository.
Symbolication: decoding native crashes
When your Expo app crashes in native code (iOS or Android), the stack trace is a list of memory addresses — completely useless for debugging. Example: [0x00f4a2c] frame 12 in libsomething.so. Symbolication is the process of mapping those addresses back to your original source files and line numbers.
Expo Prebuild generates native code from your JavaScript — when it crashes, you need the symbol map that translates the raw crash dump into readable source. Sentry and Firebase both handle symbolication automatically if you upload the right artifacts.
For iOS, upload the dSYM (debug symbols):
npx eas build --platform ios --non-interactive
# After the build, Sentry's `sentry-cli` tool will prompt you to upload dSYMs
npx sentry-cli debug-files upload --org your-org --project your-project path/to/dSYMs
For Android, upload the mapping file:
npx eas build --platform android --non-interactive
# The build generates a `mapping.txt` (ProGuard mappings) — upload this too
npx sentry-cli debug-files upload --org your-org --project your-project path/to/mapping.txt
Firebase Crashlytics handles this differently — it integrates directly with Google Play Console and App Store Connect. If you use Crashlytics and upload your builds to the stores, symbolication happens automatically. For a detailed Firebase Crashlytics setup guide, see the full platform documentation. If you sideload builds, you need to manually upload the mapping.
For Expo specifically, the eas build command integrates with Sentry's tooling. If you're on Firebase, you'll sideload or use Play Console / App Store, which works but adds friction.
Release health and crash-free rates
Once you have error tracking in place, the next layer is release health — understanding the stability of each app version you ship. This is where the comparison between Sentry and Firebase Crashlytics really matters.
Firebase Crashlytics provides a simple crash-free user percentage. You deploy a version, and Crashlytics shows you the percentage of users who didn't experience a crash. This is useful for deciding whether to roll back or push forward.
Sentry includes release tracking and session health, so you can see not just crashes but also performance regressions, error spikes, and adoption curves for each release. You can correlate "we shipped version 1.2.0" with "error rate went up 5%" and know exactly which deploy caused the problem. Release tracking across your Expo app, backend services, and job queues all in one place is powerful — one dashboard shows the health of the whole system.
Both platforms support crash-free tracking. If you're focused on that single metric, Firebase is simpler. If you want deeper visibility into release impact across your entire stack, Sentry (or LightTrace, which uses Sentry's SDK) is the better choice.
Adding context and breadcrumbs
A stack trace alone isn't enough to fix a bug. You need context: who hit the error, what were they doing, what was the app state?
import * as Sentry from "@sentry/react-native";
// Set the user so you know who hit the bug
Sentry.setUser({
id: user.id,
email: user.email,
username: user.username,
});
// Add breadcrumbs — events leading up to the crash
Sentry.addBreadcrumb({
category: "navigation",
message: "User navigated to OrderScreen",
level: "info",
});
// Tag errors so you can filter them
Sentry.setTag("checkout_flow", "guest");
Sentry.setContext("order", {
order_id: "12345",
total: 2999,
items_count: 3,
});
Breadcrumbs are the sequence of events leading to the crash — navigation, network calls, user taps, state changes. When an error hits, you see the full trail and can reproduce it without asking the user "what were you doing?"
Firebase Crashlytics supports this too, but the API is more verbose:
crashlytics().log("User navigated to OrderScreen");
crashlytics().setCustomKey("checkout_flow", "guest");
Sentry's breadcrumb system is richer and integrates automatically with many libraries (React Navigation, HTTP clients), so you get breadcrumbs with less manual work.
Performance monitoring: tracking slow frames and ANRs
Mobile error tracking increasingly includes performance data. ANR (Application Not Responding) is Android's term for a frozen app — the main thread blocks for >5 seconds and the OS shows a "force close?" dialog. Jank on iOS is similar — frame drops and UI freezes. These are critical to track because users experience them as crashes, even if the app eventually recovers.
Sentry's React Native SDK tracks performance metrics out of the box:
Sentry.init({
dsn: "https://<key>@light-trace.robomiri.com/1",
tracesSampleRate: 1.0, // Capture 100% of traces (or 0.1 for 10% sampling)
integrations: [
new Sentry.ReactNativeTracing({
tracingOrigins: ["example.com", /^\//],
routingInstrumentation: new Sentry.ReactNavigationV5Instrumentation(),
}),
],
});
This captures slow frames, navigation transitions, and slow HTTP requests. You'll see a list of slow transactions on your Sentry dashboard, sorted by duration, so you can identify which screens or API calls are dragging down user experience.
Firebase Crashlytics doesn't have built-in performance monitoring — you'd use Firebase Performance Monitoring separately, which adds complexity and another dashboard.
Sampling and cost optimization
Error tracking can get expensive if you're sending every event from millions of users. Sampling lets you capture a percentage of events, reducing costs while maintaining statistical accuracy.
Sentry.init({
dsn: "https://<key>@light-trace.robomiri.com/1",
sampleRate: 0.8, // Sample 80% of errors (and 20% of sessions)
tracesSampleRate: 0.2, // Sample 20% of transactions for performance data
});
If your app crashes once per 10k users, sampling at 10% still gives you reliable crash detection. Errors are usually low-volume anyway — a million-user app might see hundreds of crashes per day, not millions. Start at 100% (sampleRate: 1.0), monitor your event volume, then dial sampling down if you exceed your plan.
Best practices for production Expo apps
Never commit your DSN to .env files in your repository unless they're public .env files. The DSN is safe to commit (it's not a secret), but don't hardcode different DSNs for staging vs production — use environment variables or EAS Secrets.
Use EAS Secrets for environment-specific DSNs:
eas secret:create --scope project --name SENTRY_DSN_PRODUCTION
eas secret:create --scope project --name SENTRY_DSN_STAGING
Then in your app:
Sentry.init({
dsn: process.env.SENTRY_DSN_PRODUCTION || process.env.SENTRY_DSN_STAGING,
environment: __DEV__ ? "development" : "production",
});
Test your setup immediately after deploying:
import * as Sentry from "@sentry/react-native";
// In a hidden test screen or your init code
if (__DEV__) {
setTimeout(() => {
Sentry.captureException(new Error("Test error"));
}, 1000);
}
Deploy, check your dashboard, and verify the error appears with the right stack trace and symbols. This confirms your setup is working before real crashes start flowing in.
Monitor source map uploads as part of your release process. Unsymbolicated crashes are nearly useless. Make sure your CI pipeline uploads symbol maps to Sentry (or Firebase) before the app hits users. If symbols are missing, crashes show up as garbage on your dashboard and you'll waste hours trying to debug them.
Set release names consistently. Use your app version code as the release identifier:
Sentry.init({
dsn: "...",
release: "com.example.myapp@1.0.0+5", // Format: bundleId@version+buildNumber
});
This ties every crash to a specific deploy and makes it easy to know which version has a problem.
Start tracking errors in minutes
Point your Expo app at LightTrace — Sentry-SDK-compatible, $29/month for 250k events, free tier up to 5k events a month. Catch production crashes in seconds.
Error tracking is foundational for any app serving real users. Without it, you're guessing why your app crashes. With it, you're fixing bugs in minutes instead of days.