Why a Production Build Crashes When Development Does Not
A development build and a production build load your app in fundamentally different ways, and a crash that only appears in production is almost always a symptom of that difference, not a bug that was always there. In development your app pulls its JavaScript live from the Metro bundler on your machine, keeps the dev menu and error overlay, and runs with looser settings so you can iterate fast. A production build (an EAS release build, whether it goes to TestFlight or the App Store) has none of that: the JavaScript is compiled into one minified bundle shipped inside the binary, there is no dev server to reach, no red error screen, and the app runs under the same strict conditions a real user's phone enforces.
That is why the failure looks so abrupt. In development a thrown error shows you a stack trace on screen. In a release build the same error has nowhere to surface, so the app just closes on the splash screen. The crash is real and repeatable, but the information about it is not on the screen anymore. It is in a crash log. The entire job of fixing this is getting that log and reading the first line that points at your code.
Get the Crash Log Before You Change Anything
Do not start editing code on a hunch. A release-only crash names its own cause if you read the symbolicated crash report, and changing things blind usually adds a second problem on top of the first. There are three reliable places to get the log, in rough order of convenience.
- App Store Connect: for a build that reached TestFlight, open the app, go to the Crashes area under the analytics or the TestFlight build detail, and download the crash report Apple collected from a tester's device. This is the cleanest source once a build is uploaded.
- A wired device: install the release build on your own iPhone, reproduce the crash, then open the Console app on a Mac and read the device log, or use the Devices and Simulators window in Xcode to view the crash logs that phone recorded. This gives you a crash the same day, without waiting on a tester.
- A crash reporter inside the app: if you already added Sentry or a similar service, the crash is waiting in that dashboard with a symbolicated stack trace and the device details, which is the fastest path of all once it is wired in.
Whatever the source, you are hunting for the same thing: the first frame in the stack that names your own code or a specific library, not the generic system frames above it. That frame is the cause. If you have not set up a crash reporter yet, the walkthrough in [how to add crash reporting to your Expo app](/blog/how-to-add-crash-reporting-to-your-expo-app) is worth doing now, because it turns every future release-only crash from a guessing game into a named line.
Cause 1: A Missing Production Environment Value
The single most common reason an Expo app works in development and dies in production is an environment value that existed on your machine and does not exist in the release build. In development you might have a .env file, a shell variable, or a value hardcoded for testing. EAS builds run on Expo's servers, not your machine, so anything you did not explicitly pass through to the build is simply absent, and code that reads it gets undefined.
The crash pattern is telling: the app reads a value like an API base URL or a key at startup, gets undefined, and then something downstream throws because it expected a string. In Expo, values you want available inside the app at runtime go through the extra field in your app config or through EAS environment variables scoped to the build profile, not a local .env that never leaves your laptop. If a value is only defined locally, the release build has never seen it.
Cause 2: Code That Runs and Throws at Module Load
If the crash happens before any of your screens render, the error is running at import time, not inside a component. This is code at the top level of a file (outside any function or component) that executes the instant the module is loaded and throws. It survives development because the dev client is more forgiving and because your local data happened to be present; in a release build the same line runs against the real, stricter environment and fails.
Common shapes of this: accessing a property on a config object that turned out undefined in production (see Cause 1), calling a native module at the top of a file before it is initialized, or a library whose top-level code assumes something the release build does not provide. The fix is to move work that can fail out of module scope and into a place that runs after the app has mounted, so a failure becomes a handled error instead of a dead launch.
Cause 3: A Missing Native Permission or Config Value
iOS requires a usage-description string for every sensitive capability an app touches (camera, photos, location, microphone, and so on), and it enforces this at runtime in a release build. Miss the string and the moment your code reaches for that capability, iOS terminates the app. In development the same call can slip through, which is why this class of crash is a classic production-only surprise.
In an Expo app these strings are declared in your app config: under ios.infoPlist for the raw Info.plist keys, or through the config plugin of the library that needs the permission, which writes the correct key for you during the prebuild step. If you added a feature that uses the camera or location but never added its usage description, the fix is to add that string in app config and rebuild. The crash log for this names the permission in the exception message, which makes it one of the faster causes to confirm once you have the log.
Cause 4: An SDK Upgrade or Native-Module Mismatch
If the crash started right after you bumped your Expo SDK version, added a library with native code, or toggled the New Architecture, the cause is a native-layer mismatch rather than your JavaScript. A library version that does not match your SDK, or a native module that has not been rebuilt into the binary, can load fine in a cached development build and then crash a fresh release build that compiled everything from scratch.
The disciplined fix is to rebuild cleanly rather than patch around it: run the Expo dependency check to align every package with your SDK version, then produce a fresh EAS build so the native layer is compiled from the current state, not a stale cache. If you recently changed SDK versions, the [Expo SDK upgrade guide](/blog/how-to-upgrade-your-expo-sdk-version) covers doing that alignment properly so a version skew does not become a release-only crash.
How to Work the Problem With Claude Code
This is a debugging task where an AI assistant is genuinely useful, as long as you feed it the real log instead of a description of the symptom. Open Claude Code's desktop app in your project, paste in the symbolicated crash report you pulled from App Store Connect or the device console, and ask it to identify the first frame that points at your code and which of the causes above it matches. Because it can read your app config, your package versions, and the files the stack trace names, it can tell you whether the frame maps to a missing environment value, a module-load throw, a permission string, or a native mismatch.
What it cannot do is guess the cause from 'my app crashes on launch' with no log, and neither can anyone else. The terminal is there if you want to run the dependency check or kick off a fresh build yourself, but the desktop app runs those same commands for you when you describe what you want. The one input that decides everything is the crash log, so get that first, then let Claude Code map it to the fix.
The Order I Check These In
When a release build closes on the splash screen, working the causes in this order finds it fastest, because it goes from most common and cheapest to check toward least common and most involved.
Release-only launch crash: what to check, in order
| Order | Cause | Fastest confirmation |
|---|---|---|
| 1 | Missing production environment value | A config or env value read at startup is undefined in the release build but set on your machine |
| 2 | Code that throws at module load | First named frame is your own file, near the top of the bundle, and no screen ever rendered |
| 3 | Missing native permission string | Crash message names a capability like camera or location; usage description absent from app config |
| 4 | SDK upgrade or native-module mismatch | Crash started right after an SDK bump, new native library, or New Architecture toggle |
Members inside App Store Launch Club post their crash logs and their app config before they resubmit, and someone who has already chased a release-only crash reads the stack trace with them. Join at applaunchclub.co for $9 a month and bring the symbolicated log, not just the symptom.
Short, practical drops on app ideas, validating fast, store listings, and passing app review. No spam, unsubscribe anytime.
Frequently asked questions
Why does my Expo app work in Expo Go but crash in a production build?
Expo Go and a development build load your JavaScript from the Metro dev server on your machine and run with relaxed settings, while a production EAS build ships a single compiled bundle with no dev server and strict runtime enforcement. A value or permission that was present locally but missing from the release build, or code that throws at module load, will only surface in the production build. Pull the crash log and read the first frame that names your code to find which one it is.
Where do I find the crash log for a TestFlight build?
In App Store Connect, open the app and look in the Crashes section under analytics or on the TestFlight build's detail page, then download the symbolicated crash report Apple collected. You can also install the release build on your own device, reproduce the crash, and read the log in the macOS Console app or Xcode's Devices and Simulators window. A crash reporter like Sentry inside the app is the fastest source once it is set up.
My app crashes instantly with no error screen. What does that mean?
A release build has no dev error overlay, so a thrown error closes the app instead of showing a red screen. An instant close on the splash screen usually means code is throwing at module load, before any screen renders, often because it read a config value that is undefined in production. The crash log's first named frame will point at the file, and the fix is moving fallible work out of top-level module scope.
Could an Expo SDK upgrade cause a launch crash only in production?
Yes. A library version that no longer matches your SDK, or one that has not caught up to the New Architecture, can load fine in a cached development build and crash a freshly compiled release build. Run the Expo dependency check to align every package with your SDK version, then produce a clean EAS build so the native layer is compiled from the current state rather than a stale cache.
Do I need to add anything special for camera or location permissions?
Yes. iOS terminates a release build the moment code touches a sensitive capability without a usage-description string. Declare those strings in your Expo app config under ios.infoPlist, or let the config plugin of the library that needs the permission add the correct key during prebuild, then rebuild. The crash message names the missing permission, which makes this one of the quicker causes to confirm from the log.
Last reviewed by David on September 4, 2026


