Skip to content

[Android][New Arch] Awaited promises from codegen TurboModules resolve undefined in CodePush-loaded bundles (RN 0.84, bridgeless) #55

Description

@iujisato

Environment

  • react-native: 0.84.1 (New Architecture, bridgeless — RN$Bridgeless: true, __turboModuleProxy: false)
  • @code-push-next/react-native-code-push: 10.4.1
  • Hermes (embedded bundle compiled to HBC; CodePush updates are plain JS from release-react)
  • Android 16 emulator (API 36) — but the behavior is not OS-specific
  • Integration: getDefaultReactHost(context, packageList, jsBundleFilePath = CodePush.getJSBundleFile()), release build with minifyEnabled false

Summary

In a JS bundle loaded via CodePush (from the CodePush file path), awaiting a promise returned by a codegen TurboModule method resolves undefined — the await resolves at the right time, but the value is lost. Consuming the same promise with a plain .then callback delivers the correct value. The identical JS running from the embedded bundle (assets://) behaves correctly in both forms.

This silently breaks any library whose JS wrappers are async/await-based over codegen TurboModules. In our app, react-native-permissions (5.5.1) resolved undefined for every call on OTA-updated installs — notification permission handling, token registration and push delivery were all dead in production, while every local build worked.

Evidence (same device, same process where noted)

  1. Same process A/B: embedded bundle boot → checkNotifications() resolves 'granted'; CodePush restart applies the update seconds later (same PID, new JS instance) → same call resolves undefined.
  2. Bundle content exonerated: minified and unminified (--minify false) release-react bundles fail identically; Metro module resolution is byte-equivalent per sourcemaps; the failing bundle's code is correct on inspection.
  3. In-context probe inside a CodePush-loaded bundle, all in the same tick:
    const raw = TurboModuleRegistry.get('RNPermissions');
    raw.check('android.permission.POST_NOTIFICATIONS')
      .then(r => console.log(r));                    // -> 'granted'  ✅
    // whereas the library wrapper (compiled async/await over the same call):
    await check('android.permission.POST_NOTIFICATIONS'); // -> undefined ❌
  4. A lazy lookup alone does not fix it (a shim that resolves the module at call time but still awaits the native promise also gets undefined). Re-wrapping via .then does fix it:
    const toJsPromise = p => new Promise((res, rej) => { p.then(res, rej); });
    // toJsPromise(nativeModule().check(...)) is then safe to await
  5. Promises wrapped by library JS before reaching app code (axios, AsyncStorage's callback bridge, react-native-firebase's own promise layer) are unaffected — awaiting those works fine in CodePush bundles. Only raw codegen TurboModule promises lose their value under (compiled) await.

Expected

Promises returned by TurboModules should behave identically whether the bundle was loaded from assets:// or from the CodePush update path.

Workaround

Boundary shim that consumes TurboModule promises with .then callbacks and re-wraps them in new Promise(...) before the app awaits them. Works, but every affected third-party library needs its calls routed through it.

Happy to provide more diagnostics (we have an instrumented reproduction flow on a staging deployment) if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions