DEV Community

Debojit Das
Debojit Das

Posted on

How to Fix React Native EAS Build White Screens and OTA Crashes (A Complete Guide)

If you are transitioning your React Native or Expo app from traditional local builds to Expo Application Services (EAS) and Over-The-Air (OTA) updates, you might feel like you just gained a superpower.

That is, until you download your app from TestFlight and are greeted by a completely blank white screen. Or worse, the app instantly crashes before it even loads.

I recently migrated an app to EAS and ran into these exact issues. In development, everything worked perfectly. In production, everything fell apart. After hours of parsing system logs, I realized the problems weren't with my code, but with how EAS handles environment variables and OTA updates.

If your Expo app is white-screening or crashing in production, here is the complete, beginner-friendly guide to fixing it.


Part 1: The White Screen of Death (WSOD)

The Symptom

Your app works flawlessly on your local simulator. You build it for iOS or Android, install it on a physical device, and it hangs forever on a blank white screen.

If you plug your phone into your computer and check the device console, you will likely see a fatal JavaScript error that looks like this:

Unhandled JS Exception: [runtime not ready]: Error: EXPO_PUBLIC_SUPABASE_URL environment variable is not set or is empty, js engine: hermes

Enter fullscreen mode Exit fullscreen mode

Or this:

[runtime not ready]: Invariant Violation: "main" has not been registered. This can happen if:
* Metro (the local dev server) is run from the wrong folder.
* A module failed to load due to an error and `AppRegistry.registerComponent` wasn't called.

Enter fullscreen mode Exit fullscreen mode

The Root Cause: The Environment Variable Trap

When you run a local development server, your bundler easily reads your .env file to get your API keys (like your Supabase URL).

But when you build your app for production—whether on Expo's cloud servers or using a local EAS build—your .env file is left behind because it is in your .gitignore. If your app tries to initialize a database connection without those keys, the React tree silently crashes before rendering the UI.

The Fix: Properly Exposing Your Keys

Step 1: Use the EXPO_PUBLIC_ Prefix
EAS has a strict safety mechanism: it will ignore any environment variable that does not start with EXPO_PUBLIC_. This prevents you from accidentally shipping secret admin keys into your public app.
Only client-side APIs should have this prefix.

Change your code and your environment files to look like this:

EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

Enter fullscreen mode Exit fullscreen mode

Step 2: Upload Environment Variables to EAS
If you are using Expo's cloud to build your app, you need to securely pass these variables to their servers. Open your terminal and run:

eas env:create --name EXPO_PUBLIC_SUPABASE_URL --value "your_prod_url" --environment production --visibility plaintext

Enter fullscreen mode Exit fullscreen mode

(Repeat this for every client-side variable your app needs).

Once done, verify they were uploaded properly:

eas env:list --environment production

Enter fullscreen mode Exit fullscreen mode

Step 3: Link the Environment in `eas.json
You must explicitly tell your build profile to pull down the production variables you just uploaded. Check your
eas.json thoroughly. The "environment": "production"` key must exist inside your build object.

{
  "build": {
    "production": {
      "autoIncrement": true,
      "environment": "production"
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Part 2: The Local Build Caveat (--local)

To save on cloud computing costs and avoid queue times, many developers build their .ipa or .aab files locally using the --local flag.

The Trap: If you run eas build --platform ios --profile production --local, EAS isolates your project in a temporary folder. It still ignores your .env file, and because it is offline, it does not fetch the variables you uploaded to the cloud in Step 2. You will end up with another white screen.

The Fix: Before building locally, you need to create an .env.local file in your root directory.

You can either manually copy-paste your variables into it, or—especially if you are collaborating with others—pull them directly from the cloud:

eas env:pull --environment production

Enter fullscreen mode Exit fullscreen mode

Before you waste time building the full .ipa, test your production environment locally on your simulator to ensure no white screens appear:

npx expo run:ios --configuration Release

Enter fullscreen mode Exit fullscreen mode

Once that works, you can safely run your local build:

eas build --platform ios --profile production --local

Enter fullscreen mode Exit fullscreen mode

Part 3: Instant Crashes and Poisoned OTA Updates

The Symptom

You successfully fix the white screen, but after pushing an Over-The-Air (OTA) update using eas update, TestFlight users report the app instantly crashes the millisecond it opens.

If you look at the logs, you might see Apple's system daemon logging a user force-quit after a freeze:

ExceptionCode: "User Initiated Quit (0xDEADFA11)"

Enter fullscreen mode Exit fullscreen mode

Or, when trying to push the update, you get a rejection from Expo:

Manifest Validation Error:
android/intentFilters:must NOT have duplicate items (items ## 0 and 1 are identical)

Enter fullscreen mode Exit fullscreen mode

The Root Cause: Poisoned Bundles & Dirty Manifests

Unlike eas build (which uses the cloud environment), eas update bundles your JavaScript entirely on your local machine. If your local terminal doesn't have the EXPO_PUBLIC_ variables loaded, you will push an update with blank API keys.

Because your app.json has a set "runtimeVersion", every user who opens the app immediately downloads this broken update, overwriting your good code and bricking the app. You have "poisoned" the update channel.

Furthermore, EAS runs strict validation on your app.json during updates. If you have duplicate iOS background modes or Android permissions, the update will fail.

The Fix

1. Clean up `app.json
Ensure you have absolutely no duplicate entries in your
intentFilters, permissions, or UIBackgroundModes`.

2. Bump the Runtime Version
To escape the poisoned OTA loop, simply sever the link. Open app.json and change your runtime version.

"runtimeVersion": "1.0.1",

Enter fullscreen mode Exit fullscreen mode

3. Clear Cache and Update
Ensure your .env.local file is populated. Then, force Metro to clear its cache and push a clean update to the new runtime version:

eas update --branch production --clear-cache

Enter fullscreen mode Exit fullscreen mode

Finally, build a fresh .ipa or .aab. Because the runtime version changed, it will ignore the old, broken OTA update and boot normally.


The Ultimate Summary Checklist

To never face these issues again, follow this exact order of operations when setting up EAS and OTA updates:

  1. Prefix Client APIs: Ensure all front-end keys start with EXPO_PUBLIC_.
  2. Cloud Sync: Upload them to Expo via eas env:create.
  3. Local Sync: Run eas env:pull --environment production to generate your .env.local file.
  4. Configure eas.json: Ensure "environment": "production" is set in your production build profile.
  5. Clean Manifest: Check app.json for any duplicate arrays or permissions.
  6. Simulate Production: Always run npx expo run:ios --configuration Release locally first.
  7. Build: Run eas build --profile production (add --local if you want to use your own machine's hardware).
  8. Update: When pushing OTA fixes, always use eas update --branch production --clear-cache to prevent stale, broken bundles from deploying.

[ignore keywords:
"React Native TestFlight white screen of death"

"Unhandled JS Exception: [runtime not ready]"

"EXPO_PUBLIC_SUPABASE_URL environment variable is not set"

"Expo app working on simulator but crashing on TestFlight"

"EAS build local env file missing"

"ExceptionCode: User Initiated Quit (0xDEADFA11)"

"Invariant Violation: main has not been registered expo"

"Manifest Validation Error: android/intentFilters duplicate items"
]

Top comments (0)