All articles
Mobile DevelopmentJune 19, 2026 7 min read

Crash-Free Rate in React Native: Hitting 99.9% Without Going Native

How we pushed a React Native + Expo app from 99.2% to 99.9% crash-free sessions — what actually mattered, what was noise, and where Sentry, Hermes, and EAS fit in.

Crash-Free Rate in React Native: Hitting 99.9% Without Going Native

A crash-free rate of 99.2% sounds healthy until you do the math: on an app with 200,000 daily sessions, that's 1,600 broken experiences every day. We spent a quarter dragging one of our React Native + Expo apps from 99.2% to a steady 99.9%, and almost none of it involved the glamorous fixes we expected.

Here's what actually moved the number, what was theatre, and how we'd approach it again.

Why 99.9% Is the Right Target (and 100% Isn't)

Apple and Google both surface crash rates in their respective consoles, but the number most teams report internally comes from Sentry, Firebase Crashlytics, or Bugsnag. They don't all count the same way.

  • Apple's Xcode Organizer counts process crashes, including watchdog terminations.
  • Google Play Vitals distinguishes ANRs from crashes and flags you separately for each.
  • Sentry's crash-free sessions count any unhandled JS error that ends a session, which is stricter.

In our experience, hitting 100% is a vanity goal — there will always be OOMs on a 4-year-old budget Android, weird OEM ROMs that mangle WebViews, and the occasional iOS beta that ships a regression. 99.9% is the sweet spot where Play Console stops nagging you, app review stops citing stability, and your on-call rotation gets quiet.

What Counts as a Crash in a Hermes World

With Hermes as the default engine since RN 0.70-something, a JS error doesn't kill the process — it bubbles up to the React error boundary or, if uncaught, triggers a red screen in dev and a silent session-end in production. That matters for two reasons:

  1. Your native crash rate will look great because most JS errors never reach the native layer.
  2. Your session crash-free rate (Sentry's default) is the one users actually feel.

We track both. If they diverge, something is wrong with either your error boundaries or your symbolication.

Step 1: Fix Symbolication Before You Fix Anything Else

This is the boring foundation. If your stack traces look like anonymous@index.android.bundle:1:284726, you cannot prioritise anything. We lost two weeks early on chasing a phantom crash that turned out to be three separate bugs collapsed into one obfuscated frame.

For an Expo project on EAS, the source map upload should be automatic, but verify it:

# Confirm Sentry got the maps for the build you just shipped
npx sentry-cli sourcemaps list \
  --org your-org \
  --project your-app

# Cross-check against the release name EAS used
eas build:list --limit 5 --json | jq '.[].id'

The Sentry release name has to match the dist and release your app reports at runtime. We standardised on ${appVersion}+${runtimeVersion} and stopped getting mystery unsymbolicated reports.

iOS dSYMs and Bitcode-Adjacent Pain

Bitcode is gone, but iOS still needs dSYMs uploaded for any native frames. EAS does this if you have the Sentry plugin configured. If you're seeing addresses like 0x000000018a3f... in iOS reports, your dSYM upload step is failing silently — check the EAS build logs, not the Sentry dashboard.

Step 2: The Top 5 Crashes Are 80% of Your Problem

When we sorted our Sentry issues by event count, the distribution was almost perfectly Pareto. Five issue groups accounted for roughly 78% of all crash events. Going after them in order moved our crash-free rate by 0.5 points in two weeks.

What those top five usually look like in a React Native app:

  • A TypeError on a screen that loads remote data, because someone removed an optional chain after a refactor.
  • An image library throwing on malformed URLs from user-generated content.
  • A native module call with a stale ref after navigation unmounts the screen.
  • An OOM on Android from loading a full-resolution image into a list.
  • A push notification handler that assumes the app is foregrounded.

None of them are exotic. All of them are findable with Sentry → Issues → Sort by Events.

Error Boundaries Aren't Optional

The single biggest structural fix we made was wrapping every navigator screen in an error boundary that reports to Sentry and renders a recoverable fallback. Not a global app-level boundary — those just turn a crash into a white screen of death.

// ScreenBoundary.tsx
import * as Sentry from '@sentry/react-native';
import { Component, ReactNode } from 'react';
import { ErrorFallback } from './ErrorFallback';

type Props = { children: ReactNode; screenName: string };
type State = { error: Error | null };

export class ScreenBoundary extends Component<Props, State> {
  state: State = { error: null };

  static getDerivedStateFromError(error: Error) {
    return { error };
  }

  componentDidCatch(error: Error, info: { componentStack: string }) {
    Sentry.withScope((scope) => {
      scope.setTag('screen', this.props.screenName);
      scope.setContext('react', { componentStack: info.componentStack });
      Sentry.captureException(error);
    });
  }

  reset = () => this.setState({ error: null });

  render() {
    if (this.state.error) {
      return <ErrorFallback onRetry={this.reset} />;
    }
    return this.props.children;
  }
}

We wire this in at the navigator level so every screen is wrapped without each team having to remember.

Step 3: Android Is Where the Real Crashes Live

Our iOS crash-free rate was 99.7% from day one. Android was 98.9%. The gap was almost entirely:

  • OEM-specific WebView bugs (Xiaomi and certain Vivo builds were repeat offenders).
  • Background execution limits killing long-running tasks that the JS thread thought were still alive.
  • Out-of-memory on devices with under 3 GB of RAM, which is still a huge chunk of the global Android install base.

The fixes that worked:

  1. Set largeHeap only as a last resort. It masks the symptom and makes the next OOM worse.
  2. Use FastImage or expo-image with explicit resizeMode and cachePolicy. Default Image on Android decodes at full resolution.
  3. Lazy-load every modal, sheet, and rarely-used screen. A React.lazy boundary around a heavyweight chart library dropped our startup memory by a noticeable amount.
  4. Validate every push payload before touching it. We had a crash from a notification that arrived without a data field because someone fat-fingered the backend send.

ANRs Count Too

Google treats ANRs as a stability metric in Play Vitals, and they will keep you out of featured placement even if your crash rate is fine. Most React Native ANRs come from blocking the main thread during startup — usually a synchronous bridge call or a heavy MainApplication.onCreate. Move what you can to lazy initialisation and audit any native modules that do work eagerly.

Step 4: OTA Updates Are a Stability Tool, Not Just a Shipping Tool

The fastest way to fix a crash for users who already have your app is an EAS Update — but only if you've set up channels properly. We run a production-canary channel that gets 5% of traffic for 30 minutes before the full rollout. If Sentry's crash-free rate on that release drops more than 0.2 points versus the previous one, we auto-pause the rollout.

A word of caution: don't ship a JS-only hotfix if the bug is in a native module. We did this once, the OTA didn't actually fix anything, and we wasted a release cycle before remembering that runtime version changes are required for native code.

Step 5: Watch the Long Tail, Not Just the Average

Once you're past 99.5%, the global average stops being useful. Slice it by:

  • OS version — iOS 17.x vs iOS 18.x often look very different.
  • Device tier — segment Android by RAM if you can.
  • Release — your latest build might be 99.95% while an older version still in the wild is dragging the average down.
  • Geography — networks in some regions surface timeout-related crashes far more often.

Sentry's release health view does most of this for you. We pinned a dashboard to a TV in the office, which sounds like theatre but genuinely shifted how the team thinks about ship quality.

What We'd Do First on a New Project

If you're starting a React Native + Expo app today and want to bake stability in:

  1. Wire up Sentry on day one with proper source map uploads in the EAS build pipeline.
  2. Add a ScreenBoundary to your navigator template before you ship a single screen.
  3. Set up a canary EAS Update channel before your first production release, not after your first incident.
  4. Track crash-free sessions, not just crash-free users, and review them weekly.
  5. Test on a cheap Android device with 3 GB of RAM. If it survives there, it'll survive most places.

The 99.9% number isn't magic — it's the natural result of treating stability as a recurring engineering task instead of a fire drill. If you want a hand auditing an existing app, our mobile development team does this kind of work regularly, and the first thing we look at is always the symbolication pipeline.

#React Native#Expo#Stability#Sentry#Release Ops

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project