Skip to content

Babel preset: Add Hermes V1 native runtime workarounds for hermes#1761#56816

Open
ramonclaudio wants to merge 1 commit into
facebook:mainfrom
ramonclaudio:fix/hermes-v1-bug-workarounds
Open

Babel preset: Add Hermes V1 native runtime workarounds for hermes#1761#56816
ramonclaudio wants to merge 1 commit into
facebook:mainfrom
ramonclaudio:fix/hermes-v1-bug-workarounds

Conversation

@ramonclaudio
Copy link
Copy Markdown

@ramonclaudio ramonclaudio commented May 13, 2026

Summary:

Three Babel plugins for @react-native/babel-preset that work around runtime bugs in the bundled Hermes V1 (250829098.0.13, branch cut 2025-08-29). They rewrite the buggy code patterns at compile time so Hermes never sees them. Direct port of @kitten's plugins in babel-preset-expo (expo/expo#45601, merged 2026-05-11); same fix, applied at the @react-native/babel-preset layer so bare React Native consumers don't need babel-preset-expo to escape the bugs. All plugin design and implementation credit is his.

facebook/hermes#1761 was reported by trossimel-sc at Meta in August 2025, confirmed within the hour by tmikov, and fixed on Hermes static_h two weeks later in 68bfb3a48b31. The fix never got cherry-picked back to the 250829098.0.0-stable branch React Native pins. Two related codegen bugs sit in the same gap: class declarations inside finally blocks (1e94fbe0ebb4, 2026-02-12) and super.x lookups in object-literal getters and setters (18a963465944, 2025-11-04). The most recent V1 stable bump (0.13, #56728, 2026-05-08) carried only the Promise rejection-tracker backport, not these three.

  • fix-hermes-v1-async-arrow-non-simple-params: rewrites async ({a}) => 42 into async _p => { var {a} = _p; return 42; }. Without it, await resolves with undefined while the body keeps running in the background. Gated on isHermesProfile && preserveAsync since @babel/plugin-transform-async-to-generator strips async otherwise. Without this plugin, unstable_preserveAsync (Babel preset: Add unstable_preserveAsync to experiment with disabling async transforms for SH (#56254) #56254) is silently broken on Hermes V1 for any user code with non-simple async-arrow params.
  • fix-hermes-v1-class-in-finally: wraps class declarations inside finally blocks in an IIFE so the class lives in its own scope. The Hermes bug surfaces on call graphs that populate the IR variable cache between normal and exception code paths, which is recompilation-pattern dependent and hard to construct in a minimal repro. @kitten verified the workaround against Expo's internal test-suite.
  • fix-hermes-v1-super-in-object-accessor: rewrites get name() into get ["name"]() so the accessor key is computed. Identifier-keyed accessors segfault Hermes V1 during IR generation.

All three gate on isHermesProfile and bail out fast when the trigger patterns aren't present. The workaround unblocks 0.84.x and 0.85.x consumers today and becomes a no-op once a future V1 build picks up the upstream fixes.

Empirical validation:

Built a minimal bare React Native 0.85.3 repro that ports the bug-trigger patterns from @kitten's apps/test-suite work, plus a race detector, engine-identity assertions, and feature coverage for harder class shapes (static blocks, async/generator methods, private fields, getters, super-in-extends). 55 tests at ramonclaudio/hermes-1761-repro.

Per-plugin A/B isolation on iPhone 17 Pro / iOS 26.5, Hermes V1 250829098.0.13:

Config Result
All three plugins active (this PR) 55/55 PASS
Vanilla @react-native/babel-preset@0.85.3 App crashes (SIGSEGV in hermes::irgen::ESTreeIRGen::genMemberExpression)
Only super-in-object-accessor disabled App crashes in same path
Only async-arrow-non-simple-params disabled 14 async tests fail with race-detector output showing awaiter-resumed:undefined while the body returned 40
Only class-in-finally disabled 55/55 still PASS (bug surface is recompilation-pattern dependent)
iOS Release build (precompiled bytecode, no Metro) 55/55 PASS

Downstream confirmation: babel-preset-expo@56.0.7 (which carries the same plugins) applied to vanilla @convex-dev/better-auth@0.12.2 fixes the useConvexAuth.isAuthenticated race on Hermes V1. Original user-facing repro at ramonclaudio/convex-better-auth-368-repro.

Caveats:

fix-hermes-v1-class-in-finally rewrites block-scoped class declarations inside finally blocks to function-scoped var initializers. The change is observable only when code references the class identifier outside the finally, e.g., a direct reference after the finally exits or a same-named outer var the original class would have shadowed. Both are uncommon, and the four scope-delta cases I could construct are pinned as passing tests in the linked repro. The same plugin shape has been live in babel-preset-expo@56.0.6+ since 2026-05-11. Flagging in case Meta's internal test fleet surfaces an edge case the OSS test suite does not.

Changelog:

[GENERAL] [FIXED] - Work around three Hermes V1 source-level codegen bugs in @react-native/babel-preset (async-arrow non-simple params, class-in-finally, super-in-object-accessor)

Test Plan:

Test repro at ramonclaudio/hermes-1761-repro. 55/55 PASS on iPhone 17 Pro / iOS 26.5 in both Debug and Release builds. Per-plugin A/B matrix above shows each plugin's role.

Locally on packages/react-native-babel-preset:

  • 18 new inline-snapshot unit tests cover the transformed output and the skip-when-irrelevant path.
  • yarn jest packages/react-native-babel-preset clean (65 tests, 24 snapshots).
  • Full repo yarn test clean (279 suites, 5656 tests, 1816 snapshots).
  • yarn lint, yarn format-check, yarn flow-check, yarn test-typescript, yarn featureflags --verify-unchanged, yarn build-types --validate all clean.
  • Existing transform-snapshot-test is unchanged: the kitchen-sink fixture has none of the bug patterns so the new plugins don't alter its output.
  • Happy to add the three trigger patterns to kitchen-sink-input.js in a follow-up if reviewers want snapshot coverage.

References:

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 13, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label May 13, 2026
## Summary:

Three Babel plugins added to `@react-native/babel-preset` that work
around runtime codegen bugs in the bundled Hermes V1 (250829098.0.13,
branch cut 2025-08-29) by rewriting source patterns before they reach
the engine. Each plugin papers over a different bug that has been fixed
on Hermes `static_h` but has not been backported to the V1 stable
branch.

- `fix-hermes-v1-async-arrow-non-simple-params`:
  facebook/hermes#1761 (fixed in static_h by 68bfb3a48b31, 2025-09-11).
  Async arrow functions with destructured, defaulted, or rest parameters
  silently resolve `await` with `undefined` while the function body
  continues executing in the background. The plugin rewrites the params
  to a simple identifier with inline destructuring so Hermes never sees
  the buggy shape. Gated on `isHermesProfile && preserveAsync` since
  `plugin-transform-async-to-generator` rewrites the pattern away when
  `preserveAsync` is false.

- `fix-hermes-v1-class-in-finally`:
  Fixed in static_h by 1e94fbe0ebb4 (2026-02-12). Class declarations
  inside a `finally` block trip Hermes V1's variable caching path. The
  plugin wraps them in an IIFE so the class lives in its own scope.

- `fix-hermes-v1-super-in-object-accessor`:
  Fixed in static_h by 18a963465944 (2025-11-04). Object-literal getters
  and setters using `super.x` trip the `genFunctionExpression` home
  object path. The plugin marks the accessor as computed with a string
  key.

All three gate on `isHermesProfile` and bail out fast on the common
case. The plugins are a direct port of work by @kitten (Phil Pluckthun,
Expo team) in `babel-preset-expo` (expo/expo#45601, MIT licensed). All
plugin design and implementation credit belongs to him. This PR carries
his work over to `@react-native/babel-preset` so bare React Native
consumers benefit from the same fix.

## Changelog:

[GENERAL] [FIXED] - Work around three Hermes V1 source-level codegen bugs in `@react-native/babel-preset` (async-arrow non-simple params, class-in-finally, super-in-object-accessor)

## Test Plan:

- 18 new inline-snapshot test cases across three new test files verify both the transformed output and the fast-bail behavior on irrelevant patterns.
- `yarn jest packages/react-native-babel-preset` clean (65 tests, 24 snapshots).
- Full repo `yarn test` clean (279 suites, 5656 tests passed, 1816 snapshots).
- `yarn lint`, `yarn format-check`, `yarn flow-check`, `yarn test-typescript`, `yarn featureflags --verify-unchanged`, `yarn build-types --validate` all clean.
- Existing `transform-snapshot-test` passes byte-identical: the kitchen-sink fixture has none of the buggy patterns so the new plugins do not change its output for any profile.
- Verified end-to-end downstream: `babel-preset-expo@56.0.7` (which carries the same plugins) applied to vanilla `@convex-dev/better-auth@0.12.2` fixes the `useConvexAuth.isAuthenticated` race that the destructured-param async-arrow shape produces on Hermes V1.
- Public repro: https://github.com/ramonclaudio/convex-better-auth-368-repro

## Caveats:

`fix-hermes-v1-class-in-finally` rewrites block-scoped `class`
declarations inside `finally` blocks to function-scoped `var`
initializers. Observable only when code references the class by name
from outside the `finally` (rare pattern). The same plugin shape has
run in `babel-preset-expo@56.0.6+` and Expo SDK 56 preview for a week
without reported regressions, but flagging in case Meta's internal test
fleet surfaces an edge case the OSS test suite does not catch.

## References:

- facebook/hermes#1761 (root-cause issue, fixed in static_h)
- expo/expo#45601 (source of the ported plugins, by @kitten)
- expo/expo#45592 (user-facing bug report on SDK 56 preview)

Co-authored-by: Phil Pluckthun <phil@kitten.sh>
@ramonclaudio ramonclaudio force-pushed the fix/hermes-v1-bug-workarounds branch from 469ebb8 to a5fff17 Compare May 13, 2026 22:04
@ramonclaudio
Copy link
Copy Markdown
Author

ramonclaudio commented May 13, 2026

Thanks to @kitten for the original plugin work in expo/expo#45601. The original design and implementation credit on these three transforms belongs to him. This PR just ports his work over to @react-native/babel-preset so bare React Native consumers benefit from the same fix without depending on babel-preset-expo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant