Skip to content

fix(react): wrap fetchAccessToken in new Promise to fix useConvexAuth on Hermes V1#368

Open
ramonclaudio wants to merge 1 commit into
get-convex:mainfrom
ramonclaudio:fix/react-bridge-hermes-async-race
Open

fix(react): wrap fetchAccessToken in new Promise to fix useConvexAuth on Hermes V1#368
ramonclaudio wants to merge 1 commit into
get-convex:mainfrom
ramonclaudio:fix/react-bridge-hermes-async-race

Conversation

@ramonclaudio

@ramonclaudio ramonclaudio commented May 8, 2026

Copy link
Copy Markdown
Contributor

Ran into an issue where useConvexAuth().isAuthenticated never flips to true after sign-in on Expo SDK 56 canary (2026-05-05+) with React 19.2.3 and React Native 0.85.3. Better Auth session lands, /convex/token returns a JWT, but the bridge stays paused. Same on sign-up and sign-out.

Tracked it to a Hermes V1 native runtime bug with async arrow functions that have non-simple parameters (facebook/hermes#1761). The Hermes fix landed on mainline last September but isn't backported to RN's bundled Hermes. The bug only fires when the build pipeline preserves async (sends it to Hermes natively instead of transforming to generators), so it only hits some setups today:

  • Anyone on Expo SDK 56 with babel-preset-expo@<56.0.6 (the hermes-v1 config preserves async)
  • Anyone on bare React Native 0.85.x with unstable_preserveAsync: true opt-in

Wrapping the function body in an explicit new Promise(executor) sidesteps the buggy shape regardless of what the build pipeline does.

The Expo team patched it at the babel preset level (expo/expo#45601), which papers it over for Expo SDK 56 preview/stable users on babel-preset-expo@56.0.6+. I filed facebook/react-native#56816 to do the same for @react-native/babel-preset so bare RN with preserveAsync gets the same coverage, but that's on Meta's timeline. Until both framework fixes ship and propagate, this PR is the defensive workaround.

Repro: ramonclaudio/convex-better-auth-368-repro.

Test Plan

  • npm run build and npm run typecheck clean
  • npm run test unchanged (no tests under src/react/)
  • Verified on Expo SDK 56 canary 2026-05-06-03817f5 via the linked repro: vanilla 0.12.2 stalls at useConvexAuth.isAuthenticated: false, useSession.hasSession: true across sign in, sign up, and sign out. This branch's tarball flips it to true on every transition.

@vercel

vercel Bot commented May 8, 2026

Copy link
Copy Markdown

@ramonclaudio is attempting to deploy a commit to the Convex Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: get-convex/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f2cc47e0-4c41-41fe-9ad5-45e47f2899d7

📥 Commits

Reviewing files that changed from the base of the PR and between 53c46f0 and 428390d.

📒 Files selected for processing (1)
  • src/react/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/react/index.tsx

📝 Walkthrough

Walkthrough

fetchAccessToken was refactored to return an explicit new Promise wrapper. It still short-circuits to cachedToken when present (unless forceRefreshToken), reuses pendingTokenRef for concurrent callers, calls authClient.convex.token({ fetchOptions: { throw: false } }) for new requests, updates/clears cachedToken on success/failure, and always clears pendingTokenRef in finally; resolution/rejection from in-flight requests is routed through the wrapper’s resolve/reject.

Sequence Diagram(s)

sequenceDiagram
  participant Consumer
  participant fetchAccessToken
  participant Cache as cachedToken
  participant Pending as pendingTokenRef
  participant API as authClient.convex.token()

  Consumer->>fetchAccessToken: request token
  fetchAccessToken->>Cache: check cachedToken
  alt cachedToken present
    Cache-->>fetchAccessToken: return cached token
    fetchAccessToken-->>Consumer: resolve(token)
  else no cached token
    fetchAccessAccessToken->>Pending: check pendingTokenRef
    alt pending in flight
      Pending-->>fetchAccessToken: existing promise
      fetchAccessToken-->>Consumer: attach then(resolve,reject)
    else fetch new
      fetchAccessToken->>API: token({ throw: false })
      API-->>fetchAccessToken: token or error
      fetchAccessToken->>Cache: update or clear cachedToken
      fetchAccessToken-->>Consumer: resolve/reject via outer callbacks
      fetchAccessToken->>Pending: clear in finally
    end
  end
Loading

Possibly related PRs

Suggested reviewers

  • erquhart
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: wrapping fetchAccessToken in a Promise to resolve a Hermes V1 runtime issue affecting useConvexAuth authentication.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the root cause (Hermes V1 async bug), the fix strategy (Promise wrapper), and verification steps.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: one or more packages not found in the registry.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dowski

dowski commented May 13, 2026

Copy link
Copy Markdown

Might this be fixed upstream in the Expo 56 preview?

expo/expo#45592

@ramonclaudio

Copy link
Copy Markdown
Contributor Author

Might this be fixed upstream in the Expo 56 preview?

expo/expo#45592

@dowski the expo team did patch it upstream, but only at the babel preset level (expo/expo#45601). That covers SDK 56 users on babel-preset-expo@56.0.6+, which ships by default in expo@next (preview.8+). Anyone on expo@canary still hits the bug because that tag froze at a pre-fix snapshot, but they can pin "babel-preset-expo": "~56.0.7" via npm overrides as a manual workaround. Same goes for bare React Native users who opt into unstable_preserveAsync.

Filed facebook/react-native#56816 to port the same babel transforms into @react-native/babel-preset so bare RN gets the same coverage, but that's on Meta's timeline and there is no guarantee on if/when it lands.

Leaving it up to @erquhart to decide what to do here!

@erquhart

Copy link
Copy Markdown
Member

Thanks Ray - holdilng this until expo 56 is closer to stable, would hope this would be addressed upstream.

… on Hermes V1

The /convex/token response triggers a session rotation (via Better
Auth's Set-Cookie processing) plus a setCachedToken call inside the
bridge's .then. The next render rebuilds fetchAccessToken's
useCallback (keyed on [sessionId]) and fires
ConvexAuthStateFirstEffect's client.setAuth a second time.

On Hermes V1 native async (Expo SDK 56 canary 2026-05-05+ since
expo/expo#45345 dropped @babel/plugin-transform-async-to-generator),
that second setAuth lands inside the first setConfig's await window
in authentication_manager.ts. fetchTokenAndGuardAgainstRace bumps
configVersion on entry and the original await sees the stale value,
returning isFromOutdatedConfig: true. setConfig bails without
resumeSocket() and the chain repeats.

Drop the async keyword and wrap the body in new Promise(executor)
directly. The constructor's resolve(thenable) schedules a
NewPromiseResolveThenableJob microtask, the same hop regenerator's
_asyncToGenerator provides. With the hop in place the second setAuth
lands after the first setConfig finishes rather than during its
await window.
@ramonclaudio ramonclaudio force-pushed the fix/react-bridge-hermes-async-race branch from 53c46f0 to 428390d Compare June 2, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants