Skip to content

fix(mobile): apply RN ExceptionsManager Hermes fix via patch-package - #14643

Merged
dylanjeffers merged 1 commit into
mainfrom
fix/mobile-rn-exceptions-patch
Oct 9, 2026
Merged

dylanjeffers merged 1 commit into
mainfrom
fix/mobile-rn-exceptions-patch

Conversation

@dylanjeffers

@dylanjeffers dylanjeffers commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What the patch does

#13609 added a fix to React Native's Libraries/Core/ExceptionsManager.js. It adds a getErrorStackSafe() helper that reads .stack inside a try/catch, and returns it only when it's a string. The helper replaces the three places where the exception handler reads .stack directly:

  • parseErrorStack(e?.stack) in reportException
  • rawStack: e.stack in reportException
  • if (firstArg?.stack) in reactConsoleErrorHandler

On Hermes, reading .stack on a value that inherits from Error in the legacy way (Foo.prototype = new Error()) throws Error.stack getter called with an invalid receiver. When that happens inside the global exception handler, the original error gets lost and the handler throws again. See react/react-native#43636 and facebook/hermes#1496.

Why it never applied

packages/mobile/scripts/patch-react-native-exceptions.js looked for packages/mobile/node_modules/react-native/.... npm hoists react-native to the root node_modules (it's in the root of package-lock.json on both 0.79.5 and 0.81.6). When the file wasn't found, the script ran process.exit(0), so postinstall printed "Patching React Native ExceptionsManager" and changed nothing. On main the installed ExceptionsManager.js is byte-identical to the npm tarball, so this fix has never shipped.

Fix

  • Moved the same edits into patches/react-native+0.79.5.patch.
  • Deleted the custom script and its block in scripts/postinstall.sh.

scripts/patch-package.sh already runs root patches with --error-on-fail. So if react-native stops being installed at the root, or an RN upgrade changes the lines being replaced, npm install / postinstall now fails instead of silently doing nothing. The diff is identical to the old script's output, generated from a pristine 0.79.5 tarball.

RN 0.81 (#14642)

All four anchor lines are unchanged in 0.81.6. This patch applies cleanly to 0.81.6 (output byte-identical to a patch generated against 0.81.6), with patch-package's version-mismatch warning only. The upgrade PR should rename it to react-native+0.81.6.patch once this lands.

Verification

  • patch-package 6.5.1 in a scratch project:
    • applies to pristine 0.79.5 and to 0.81.6, exit 0
    • exits 1 when react-native is missing from node_modules/
    • exits 1 when a replaced line has changed (simulated drift)
  • bash scripts/patch-package.sh in a worktree with cloned node_modules:
    • every patch reports ✔, exit 0
    • a second run (the CI cache-hit path, which reruns postinstall) also exits 0
  • The patched file parses with hermes-parser.
  • The patched file compiles with @react-native/babel-preset; the output contains the helper and its 3 call sites.
  • No native build. This is a JS-only change that Metro bundles, so it ships in the next build or OTA.

Behavior change to note: this is the first time the fix ships. rawStack and the firstArg check now treat a non-string .stack as absent. Before, they passed it through or treated it as truthy.

🤖 Generated with Claude Code

patch-react-native-exceptions.js targeted packages/mobile/node_modules/react-native,
but react-native is hoisted to the root node_modules, so the script exited 0 without
patching anything. Replace it with patches/react-native+0.79.5.patch, which
patch-package --error-on-fail applies in postinstall and fails on if it stops applying.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 8, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 76e3b41

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dylanjeffers
dylanjeffers merged commit f7c1094 into main Oct 9, 2026
3 checks passed
@dylanjeffers
dylanjeffers deleted the fix/mobile-rn-exceptions-patch branch October 9, 2026 00:00
dylanjeffers added a commit that referenced this pull request Oct 9, 2026
Phase 1 of the React Native upgrade: 0.79.5 → 0.81.6 with the old
architecture still on.

**Base: `chore/mobile-native-release-1.5.188` (#14654).** Rebased onto
it after 1.5.187 (#14645, which squashed #14627, #14640 and #14650)
merged to main; the two lib-swap commits are gone from this branch.
Merge #14647 and #14651 into this branch first, then this into #14654,
then #14654 into main as one push (see #14654 for the full order).

## Rebase onto 1.5.187 (2026-10-08)
- `patches/react-native+0.79.5.patch` (the ExceptionsManager Hermes fix
from #14643) is renamed to `react-native+0.81.6.patch`. The anchors are
unchanged in 0.81.6, and `scripts/patch-package.sh` applies it with
`--error-on-fail` during `npm install` (checked: `getErrorStackSafe` is
in the installed `ExceptionsManager.js`).
- notifee and Firebase from #14640 are kept as they are:
`@notifee/react-native` 9.1.8 is the latest release and
`@react-native-firebase/app`/`messaging` 24.1.1 build on 0.81.6 on the
old arch (both declare `react-native: *`). No bump needed.
- The Firebase Android BoM stays pinned to 33.16.0. This branch moves
Kotlin to 2.1.20, and BoM 34 needs 2.2, so the pin still applies; the
comment now says so.
- The UIScene and notifee lines in `AppDelegate.mm` (#14626, #14640) are
untouched by this branch.
- `package-lock.json` and `Podfile.lock` were regenerated (`npm
install`, then `RCT_NEW_ARCH_ENABLED=0 bundle exec pod install`); the
release xcconfig has no `RCT_NEW_ARCH_ENABLED=1`. The SwiftAudioEx
post_install patch (#14650) still applies.

**Needs a native release (1.5.188).** Don't OTA this JS onto older
binaries. Old architecture stays on: Android `newArchEnabled=false`, iOS
`RCTNewArchEnabled=false`, pods installed with `RCT_NEW_ARCH_ENABLED=0`.

Two commits, one per step. Each step builds on iOS and Android:
1. 0.79.5 → 0.80.3
2. 0.80.3 → 0.81.6 (0.81.6 is the latest 0.81 patch on npm)

## Version bumps

| Package | From | To | Why |
|---|---|---|---|
| react-native | 0.79.5 | 0.81.6 | the upgrade |
| `@react-native/*` (babel-preset, eslint-config, metro-config,
metro-babel-transformer, typescript-config) | 0.79.5 | 0.81.6 | must
match RN |
| `@react-native-community/cli`, `cli-platform-android`,
`cli-platform-ios` | 18.0.0 | 20.1.3 | CLI line for 0.81; the platform
packages were left at 18 on the old ladder branch |
| react, react-dom, react-test-renderer (whole monorepo, root
`overrides` too) | 19.0.0 | **19.1.4** | RN's bundled renderer throws at
startup unless `react` is **exactly** its version: 19.1.0 for 0.80.3,
19.1.4 for 0.81.6. The old ladder branch used 19.1.8, which would throw
"Incompatible React versions" on launch. |
| @types/react / @types/react-dom | 19.0.0 | 19.1.17 / 19.1.11 | match
React 19.1 |
| react-native-gesture-handler | 2.25.0 | 2.28.0 | 2.28 is the first 2.x
with official 0.81 support (2.25's table stops at 0.79). Stays on 2.x;
v3 drops the old architecture. |
| @amplitude/analytics-react-native | 1.4.11 | 1.4.14 | Kotlin 2.1 (RN
0.80+) turns the deprecated `toUpperCase(Locale)` in 1.4.11 into a
compile error |
| tiktok-opensdk-react-native | ^0.10.7 | 0.10.7 (pinned) | it now
carries a patch. 0.10.9 fixes the Android signature, but drops the
`handleOpenURL`/`handleUserActivity` declarations that SceneDelegate
imports |

Patches added (`patches/`):
- `react-native-track-player+4.1.2.patch`: Kotlin 2.1 rejects
`Arguments.fromBundle(Bundle?)`. Now resolves `null` when there is no
item. 4.1.2 is the last 4.x and has no fixed release.
- `tiktok-opensdk-react-native+0.10.7.patch`: `onNewIntent(intent:
Intent)` (RN 0.80 made `ActivityEventListener` non-null) and
`reactApplicationContext.currentActivity`.

Checked against 0.81 on the old architecture and left alone: Reanimated
3.19.5 (the official table lists 3.19.x for 0.78–0.81 on Paper;
Reanimated 4 is out of scope), track-player 4.1.2 (patched above),
collapsible-tab-view 8.0.1 (JS only), screens 4.18.0 (0.81 support since
4.14; `react-native-screens+4.18.0.patch` still applies; 4.25+ drops the
old arch), pager-view 6.7.1 (7+ drops the old arch), video 6.18.0, svg
15.15.0 (0.81 support since 15.12.1), google-cast 4.6.2 (5.x is new-arch
only), notifications 5.1.0, code-push 12.3.2 (README lists 0.77–0.86;
13.x needs a new OTA history), bootsplash 6.3.11, safe-area-context
5.6.2, keyboard-controller 1.19.0, flash-list 1.8.3, datetimepicker
8.3.0 (builds against 0.81.6).

## Template changes (rn-diff-purge 0.79.5 → 0.81.6)
- Android: Kotlin 2.0.21 → 2.1.20, Gradle 8.13 → 8.14.3, new
`gradlew`/`gradlew.bat`/wrapper jar, `MainApplication` uses
`loadReactNative(this)` in place of `SoLoader.init` plus `load()`, and
`edgeToEdgeEnabled=false` is added as in the template. The app already
draws edge-to-edge through react-native-bars and the SDK 36 target, so
this flag changes nothing.
- AGP comes from RN: 8.8 → 8.11.0, which supports compileSdk 36, so
`android.suppressUnsupportedCompileSdk=36` is removed.
- `settings.gradle` resolves `@react-native/gradle-plugin` through
`require.resolve` from `react-native`, not a hard-coded hoisted path.
With this lockfile npm nests it under
`node_modules/react-native/node_modules`.
- Kept: `newArchEnabled=false`, the `AudiusScrypt` module
(`ScryptPackage` is still registered), the debug manifest (it adds
`SYSTEM_ALERT_WINDOW`), and
`android:enableOnBackInvokedCallback="false"`.
- **Back handling:** since 0.80, `ReactActivity` registers an
`OnBackPressedCallback` when targetSdk ≥ 36, which calls
`onBackPressed()` → JS `BackHandler`. Our `invokeDefaultOnBackPressed`
override (`moveTaskToBack`) still runs. The manifest opt-out is kept as
asked and can probably go in a later release.
- iOS: no template change between 0.79.5 and 0.81.6. `Info.plist` and
`RichPushExtension/Info.plist` now set `RCTNewArchEnabled=false`, which
RN 0.80+ reads **at runtime**; a missing key means new arch. `pod
install` writes the key and re-sorts the plist each time, so the sorted
version is committed.

## AppDelegate / UIScene
No code change needed. `RCTAppDelegate` is marked deprecated in 0.81 in
favour of `RCTReactNativeFactory`, so there is a
`-Wdeprecated-declarations` warning, but every API we use is unchanged:
`automaticallyLoadReactNativeWindow`, `rootViewFactory`,
`createRootViewController`, `setRootView:toRootViewController:`,
`customizeRootView:`, `bundleURL`, and `dependencyProvider`.
`didFinishLaunching` still creates the factory and skips the window.
Moving to `RCTReactNativeFactory` can wait for the 0.82+ phase.

Verified on a Release build: cold launch, a deep link while killed, and
a deep link while backgrounded, on iOS 26.5 and iOS 27. iOS 27 needs
UIScene and showed no launch crash.

## Web / React 19.1
- Typecheck: web, common and harmony are clean, and so is mobile.
- `vite build` (prod env): succeeds.
- web vitest: 178 passed, 9 skipped. `TrackTile › Public Premium
(non-owner)` timed out once while the machine was under heavy load, then
passed 3 out of 3 when run alone.

## Verification matrix (before the rebase; see the bottom for the
rebased stack)
| | iOS 26.5 sim (Release) | iOS 27 sim (Release) | Android API 36 emu
(prodRelease, arm64) |
|---|---|---|---|
| Build | ✅ Xcode 27, `generic/platform=iOS Simulator` | same binary | ✅
debug keystore |
| Old arch at runtime | ✅ JS thread `com.facebook.react.JavaScript` | –
| ✅ `ReactRootView` + "Legacy Architecture" warning |
| Launch past splash | ✅ | ✅ | ✅ `assets://index.android.bundle` |
| Sign-up / sign-in screens | (signed in) | ✅ | ✅ including Create
Password |
| Signed-in feed, scroll | ✅ | – | – |
| Playback + now playing drawer (open, drag closed) | ✅ | – | – |
| Background → foreground while playing | ✅ audio kept going (0:09 →
0:41) | – | – |
| Search | ✅ | – | – |
| Profile: collapsible header + tab swipe | ✅ | – | – |
| Deep link, warm | ✅ `audius://audius` | ✅ link delivered | – |
| Deep link, cold | ✅ `audius://deadmau5` | ✅ (signed out → sign-on) | –
|
| Hardware Back | – | – | ✅ pops Create Password; at root backgrounds
the app without killing it |
| Sign-on footer above nav bar | – | – | ✅ |
| Red JS errors | none, apart from a recurring `JSON Parse error:
Unexpected character: <` (not yet traced) | none | none |

The CodePush folder was moved aside or cleared before every cold launch,
so all results above are from the bundled JS.

## Still needs a real device
- background audio with the screen locked for more than 2 minutes
- lock-screen and notification controls
- Chromecast
- push notifications

## Notes
- A Release build that downloads a production OTA built for 1.5.186 or
1.5.187 runs a 0.79 bundle on a 0.81 binary. 1.5.188 ships with its own
package.json version (#14654) so it never receives those OTAs.
- The Hermes `ExceptionsManager` fix now applies through patch-package
(#14643), renamed for 0.81.6 above.

## Verification of the rebased stack (2026-10-08)
Built from a local, unpushed integration branch: #14652's tip (which
contains #14647) merged with #14651, on top of #14654.

| Check | Result |
|---|---|
| `npx tsc --noEmit -p packages/mobile` | pass |
| eslint on the touched files / `turbo run verify` lint | pass (1
existing warning in `HostRemixContestDrawer.tsx`) |
| `cd packages/mobile && npm test` | 9 suites, 34 tests pass |
| iOS Release, `generic/platform=iOS Simulator`, arm64,
`ENVFILE=.env.prod` | builds; Info.plist 1.1.201,
`RCTNewArchEnabled=false`; bundle carries 1.5.188 |
| Android `assembleProdRelease`, arm64-v8a, debug keystore | builds;
versionName 1.1.537 |
| iOS 26.5 sim, installed over the signed-in app, CodePush moved aside |
launch to feed; JS thread `com.facebook.react.JavaScript` (old arch) |
| Playback from the 2nd feed track | plays; play bar tracker moves |
| Auto-advance | seek to 2:43/2:45 on track 2, advanced to track 3 |
| Now playing drawer | opens, scrubber advances, tap-to-seek works |
| Profile tabs | swipe Tracks → Albums, tap Reposts |
| notifee/RNFB APNs token | a temporary console log (not committed,
rebuilt without it afterwards) printed an 80-byte lowercase APNs token
at startup |
| Android 16 emulator, `pm clear` | `Loading JS bundle from
"assets://index.android.bundle"`, "Legacy Architecture" warning, sign-up
and sign-in screens render |
| Android hardware Back | **not verified**: the emulator's system_server
was killed by its watchdog three times under host load, so Back never
got a clean run |
| JS errors on iOS | only `Could not cache profile images` (content node
timeout) |

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant