App performance and reliability

How to reduce app crashes

Reduce app crashes by making every crash report readable, fixing the issues that hit the most users first, reproducing each one and checking the fixed release.

12 min read

Quick answer

To reduce app crashes, first make every crash report readable by uploading source maps and debug symbols for each build. Rank issues by the users and sessions they affect, reproduce the top one under the same device and data conditions, and fix the cause. Then compare crash-free sessions for that issue in the fixed release. Track hangs (ANRs) and memory terminations separately.

Support forwards a ticket: "App crashed when I opened my vacation album. Third time this week." Your crash dashboard shows nothing for that user. The process might have thrown an exception, stopped responding, or been killed by the OS because the phone ran low on memory, and each of those needs different work from you.

Step 1: Sort reports into crashes, hangs and memory terminations

Decide what ended the user's task before you pick a fix. Android documents crashes and application not responding (ANR) errors separately. On iOS, a memory-related termination produces a jetsam event report instead of an ordinary crash trace.

Failure typeWhat you need to diagnose itWhere to look first
Unhandled JavaScript or app exceptionA symbolicated stack trace and the state around itA wrong assumption or a missing error path
Native crashThe native crash report and matching debug symbolsA native library, memory safety or platform integration
Hang or ANRThread state and timingBlocking work on the main thread or a stalled dependency
Memory terminationMemory diagnostics and device conditionsLarge allocations or resources that are never released
Recoverable operation failureError context and the failed user actionNetwork or service behavior that did not end the process

One SDK rarely captures every row of that table, so check which ones yours covers.

Ask support to collect the action the user took and roughly when, and nothing sensitive you do not need. Match that against the release and device in your crash reporter.

Keep the goal in view: users finishing their tasks. An error screen that prevents data loss is a better outcome than silently continuing after an unexpected state, even though neither one shows up as a crash.

Step 2: Make every crash report readable

A minified JavaScript stack or a raw native address is hard to act on. Save the files that map each build back to source: the source map for the exact JavaScript bundle, plus dSYMs on iOS and ProGuard mapping files on Android. A map from a different build gives you a plausible but wrong line.

Record the build and the update separately if you ship JavaScript over the air. The installed binary can stay the same while the running bundle changes, and you need both IDs to know which code failed. With Sentry and EAS Update, Expo's Sentry guide uploads update source maps in the same command:

Attach enough context to rebuild the operation, such as a route name or operation type, and leave out request bodies. Scrub account details and secrets before anything leaves the device.

Test the reporting pipeline in a release build. Throw a deliberate error, then check that it reaches the dashboard with a readable trace and the right release attached.

Write down what your reporter cannot see. Some reporters, EAS Observe among them, send a fatal crash on the next launch, so a user who never reopens the app sends nothing. Some terminations happen before anything is saved. A quiet dashboard does not mean nobody crashed.

Step 3: Rank issues by affected users and blocked tasks

Event count alone misleads. One install that crashes on every launch can produce many events, while a crash that blocks every first purchase may produce fewer events and cost you more.

Use crash-free users and crash-free sessions on purpose. Firebase's definitions are typical: crash-free sessions is the share of sessions that did not end in a crash, and crash-free users is the share of users who engaged with the app without one. If 20 of 1,000 sessions crash, the crash-free session rate is 98%.

Google Play publishes bad behavior thresholds you can use as an Android floor: a user-perceived crash rate of 1.09% of daily users and a user-perceived ANR rate of 0.47%. On any single device model, the threshold for either is 8%. Apps above a threshold are likely to be less discoverable on Google Play.

Do not average daily percentages without the counts behind them. A small new-release cohort and a large old-release population also differ in device mix, so compare like with like.

Rank by impact on the user. Put crashes at startup, crashes that stop an important action and crashes on a device you have committed to support at the top.

Keep new issues separate from old ones. A long-standing crash with a high lifetime count can hide a regression the latest release introduced. Once the list is sorted, give the top issue an owner and a plan to reproduce it.

Step 4: Reproduce the crash on the same device and data

Open the same feature with the state that led to the failure. A photo screen with one small image does not reproduce a crash that happened after loading a large gallery.

Rebuild the device constraints and app state. Test the upgrade path when the report came from an existing user, and check whether a stale database record or an expired session triggers the failure.

For a suspected memory problem, repeat the action instead of looking at one snapshot. If each visit keeps another resource alive, the first visit looks healthy. Leave the screen and check whether memory comes back.

For a hang, find the work that stops the app from responding, using Instruments or the Android Studio profiler. Moving code around without knowing its dependencies can turn a hang into a race condition.

For a crash triggered by a network response, make a test server return that exact shape or status code. Waiting for the production outage to happen again with a debugger attached is not a plan.

Write down the shortest sequence that still crashes. That sequence becomes your regression test, and it stops a broad refactor from fixing the bug by accident.

If you cannot reproduce it, write down what the data shows and what is still a guess. Add narrowly scoped diagnostics instead of logging every field you can think of.

Step 5: Fix the cause and test the recovery path

Keep the user able to recover. Catching an exception and reporting success removes the crash and quietly breaks the action.

Fix at the source. Validate an invalid server response where it enters the app and show a clear failure state. For a missing native capability, correct the build or the dependency. For a resource leak, find the owner and release it there.

Keep a regression record like this one:

Test next to the failure as well as at it. A memory fix can change image quality or loading. A new error boundary can leave the user stuck on a screen. Confirm that the recovery action works and that the process keeps running.

Step 6: Ship the fix through the right channel

A fix that changes native code needs a new native build and a store release. A fix in JavaScript or assets can ship as an over-the-air update, as long as it is compatible with the native runtime already installed and stays within store rules, including Apple's App Store Review Guideline 2.5.2.

Roll out in stages where you can, and decide in advance what you will do if the crash continues. Pausing a staged rollout does not remove the release from phones that already installed it, so have a replacement release or a compatible update ready.

Step 7: Confirm the fix with the same definitions

Compare the same crash group and the same denominator in the fixed release. A falling issue count after rollout can mean traffic fell too. Allow for reporting delay, note the cohort size and watch for a new crash the fix introduced.

Small numbers need care. Say the broken release recorded 20 crashes in 10,000 sessions and the fixed release recorded 2 in 1,000. Both are 0.2 crashes per 100 sessions, so the smaller count shows no improvement yet. That rate counts crash events, not the share of sessions or people affected.

State the observation window and cohort next to the result. If the fixed release reached newer devices first, the comparison may reflect exposure instead of the fix. "The original reproduction now passes, and production data is still limited" is an honest status. Keep watching until enough users have the release, against a threshold you chose before you looked.

Every few weeks, look for repeated causes. If missing source maps slow down every incident, fix the build process. If malformed responses keep crashing screens, fix the shared data validation and its tests.

Where Expo fits

EAS Observe has error reporting in preview on SDK 57. It records unhandled JavaScript errors automatically, ObserveErrorBoundary catches render errors and shows a fallback screen, and Observe.reportError records errors you handle yourself. From expo-observe 57.0.21 it also records native crashes: Java and Kotlin exceptions and, on Android 11 and later, OS crash records for native code; MetricKit crash reports on iOS. The Errors page shows crash-free sessions and users per release.

Set uploadSourceMaps in your build profile and EAS Build stores the source map for every build, so JavaScript traces point at your file and line:

EAS Observe does not symbolicate native stack traces yet, so pair it with a dedicated crash reporter such as Sentry or Firebase Crashlytics for native crashes. Expo's product page for Observe describes the two as complementary: Observe owns the view of the EAS pipeline, and tools like Sentry own error tracking.

Expo's Sentry guide uploads source maps automatically for EAS Build, and the one-line command above covers EAS Update. After you link your EAS project to your Sentry project, crash reports show up in the EAS dashboard on each update deployment.

EAS Update adds a safety net for a broken JavaScript update. Under error recovery, expo-updates watches for a fatal error on an update's first launch, before its first screen renders. When that happens, it marks the update as failed, checks for a newer one for 5 seconds, and otherwise rolls back to the last update that launched successfully. You can also republish a known-good update from the EAS dashboard or CLI.

Limitations

EAS Observe error reporting is in preview, and it does not record ANRs or out-of-memory terminations. Native stack traces are not symbolicated in the dashboard: iOS frames show function names without file or line numbers, and you cannot upload dSYMs or ProGuard mappings yet. Errors from an OTA update show unsymbolicated JavaScript traces until source maps for EAS Update ship, and source map upload needs EAS CLI 22.0.0 or later and a build on EAS Build servers.

Expo says the error recovery behavior is subject to change and not to rely on it, so treat it as a backstop, never as your rollback plan. A native fix always needs a new build and store review, and a green build does not prove the app logic cannot crash.

Next step

Pick the crash that blocks the most users and use the Android crash guidance or your crash reporter to build a reproducible case tied to its exact release.

Verified on 12 September 2026.

Keep reading

Frequently Asked Questions