Case study · October 2026
The launch crash App Review rejected, and the fix that hid it
A consumer app in production on both stores, on React Native 0.77, had a release rejected for crashing at launch. The emergency fix made the crash go away. It did not fix it. This is what was really wrong, and the other problems standing between the app and a supported React Native.
Before
- React Native 0.77.1, out of support
- New Architecture switched off to stop a crash
- App Store release rejected
- Upgrades past 0.81 impossible
After
- React Native 0.80.3 and React 19.1
- New Architecture on, iOS and Android
- Root cause of the rejection fixed
- Clean path to the next upgrade
1. The rejection: a crash that only happens in Release
App Review rejected the build for crashing on launch. React Native 0.76+ turns the New Architecture on by default, so the team switched it off in the Podfile and shipped again. The crash disappeared, and the diagnosis in the commit blamed a navigation library.
Turning the New Architecture back on and running the app on a simulator showed the real symptom. Not a crash in Debug, just one error line:
Error: Failed to call into JavaScript module method RCTEventEmitter.receiveEvent().
Module has not been registered as callable. Registered callable JavaScript modules
(n = 9): AppRegistry, HMRClient, GlobalPerformanceLogger, RCTNativeAppEventEmitter, ...
In Debug React Native logs it. In Release it is fatal. That is why it only showed up for App Review.
To see which component sent the event, we temporarily registered a stub for the missing module and logged what arrived:
'NKDIAG receiveEvent', 2, 'topInsetsChange', '{"frame":{"width":402,"height":874,"x":0,"y":0},"insets":{"bottom":34,"right":0,"top":62,"left":0}}'
topInsetsChange comes from SafeAreaProvider. The library ships a proper New Architecture component, and it was compiled into the app, but React Native was not using it: it was running the old version through the compatibility layer, whose events go to a module that no longer exists without the bridge.
AppDelegate.mm. The app had been upgraded without it, so no third-party component was registered with the New Architecture.#import <ReactAppDependencyProvider/RCTAppDependencyProvider.h>
...
self.dependencyProvider = [RCTAppDependencyProvider new];
With that line: zero legacy events, no error, the New Architecture back on. Switching it off had only hidden the bug until the next time someone switched it on.
2. Android: alive, but a red screen
On a real Android emulator the app process stayed alive. A test that only checks "did it crash" would have passed. The screen said otherwise:
Exception in HostObject::get for prop 'TrackPlayerModule':
TurboModuleInteropUtils$ParsingException: Unable to parse @ReactMethod annotation from native
module method: TrackPlayerModule.clearNowPlayingMetadata(). Detected unsupported return class:
kotlinx.coroutines.Job
The audio library writes its methods as fun play(...) = scope.launch { }, which returns a coroutine Job. The New Architecture compatibility layer only accepts methods that return nothing, so the whole module failed to load. Because it is imported at startup, the app never registered. iOS was fine; the iOS module is written differently.
The latest stable release of the library has the same pattern in 36 methods, so upgrading would not help. A 25-method patch that keeps the same launch but returns nothing fixed it.
3. Deleting is cheaper than upgrading
Four packages were installed and never used: never imported, never referenced from native code, not required by another package. One of them was a Firebase database SDK. On iOS it pulled in gRPC, BoringSSL and abseil, some of the slowest and most fragile pods to build. Removing it took 1,361 lines out of Podfile.lock.
4. The upgrade itself: what breaks between 0.77 and 0.80
Library versions came from each library's own compatibility table, not from "latest": react-native-screens 4.18 (4.25+ drops the old architecture, 4.26+ needs React Native 0.84), react-native-reanimated 3.19 (4.x is a separate migration), react-native-gesture-handler 2.28 (2.32+ needs 0.84). Then, in order:
| What broke | Why |
|---|---|
pod install: cannot load native_modules | The Podfile still loaded a file the new CLI no longer ships. |
Android build: no suitable method found for getDefaultReactHost | A new parameter whose default Java code cannot see. |
iOS build: 'folly/coro/Coroutine.h' file not found | Compiler flags in the Xcode project came from a much older template. |
| SVG library fails to compile on both platforms | A patch written to make it work on 0.77 undid exactly what 0.80 needs. Deleted. |
| A patch silently stopped applying | It had captured 350 KB of build output. It worked on the machine that made it and would have failed on CI and on every new machine. Recreated: 12 KB. |
| Kotlin 2.1 compile errors in the audio library | Stricter nullability and an Android API signature. Added to the patch. |
Every step ended with builds on both platforms and a launch on an iOS simulator, and from the New Architecture switch on, an Android emulator too: process alive, no JavaScript errors in the log, screen rendered.
Every one of these is now a check
Problems found on real upgrades go back into the tool. Run against the original repository today, NativeKeel flags all of the following before any work starts:
| Problem | Would have cost | |
|---|---|---|
Missing RCTAppDependencyProvider | A rejected release | ✓ flagged |
| Native module that cannot load on the New Architecture | Red screen on Android only | ✓ flagged |
| Unused native modules, incl. the gRPC-heavy SDK | Build time, upgrade surface | ✓ flagged |
| Podfile loading the old CLI file | pod install fails at 0.80 | ✓ flagged |
| Stale folly flags in the Xcode project | iOS build fails at 0.80 | ✓ flagged |
Two-argument getDefaultReactHost in Java | Android build fails at 0.80 | ✓ flagged |
| patch-package patches that must be re-checked at every hop | A library broken by its own patch | ✓ flagged |
| Flipper still wired in (not 16 KB aligned) | Google Play rejection risk | ✓ flagged |
| Release signing passwords committed to git | Anyone with repo access can sign | ✓ flagged |
None of these show up in a debug build on the developer's machine. That is the point.
Run it on your app
One command, no signup, nothing leaves your machine:
npx nativekeel
If the report says "Extra large", we do fixed-price upgrades like this one on a branch of your repository, delivered as a pull request with test builds. No calls needed.
See a sample reportUpgrade service