← Home

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.

14problems fixed, each verified by builds on both platforms
0.77 → 0.80React Native, with React 18 → 19
43 → 38native modules; Podfile.lock −1,361 lines
~2 hengineering time, build waits included

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.

Root cause: React Native 0.77 requires one new line in 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.

Near miss: a fifth package also looked unused. Removing it would have built fine and crashed at runtime: another UI library requires it as a peer dependency. "Unused" has to mean unused by everything, not only by your code.

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 brokeWhy
pod install: cannot load native_modulesThe Podfile still loaded a file the new CLI no longer ships.
Android build: no suitable method found for getDefaultReactHostA new parameter whose default Java code cannot see.
iOS build: 'folly/coro/Coroutine.h' file not foundCompiler flags in the Xcode project came from a much older template.
SVG library fails to compile on both platformsA patch written to make it work on 0.77 undid exactly what 0.80 needs. Deleted.
A patch silently stopped applyingIt 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 libraryStricter 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:

ProblemWould have cost
Missing RCTAppDependencyProviderA rejected release✓ flagged
Native module that cannot load on the New ArchitectureRed screen on Android only✓ flagged
Unused native modules, incl. the gRPC-heavy SDKBuild time, upgrade surface✓ flagged
Podfile loading the old CLI filepod install fails at 0.80✓ flagged
Stale folly flags in the Xcode projectiOS build fails at 0.80✓ flagged
Two-argument getDefaultReactHost in JavaAndroid build fails at 0.80✓ flagged
patch-package patches that must be re-checked at every hopA 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 gitAnyone 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