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)
- 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.
- 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.
- 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 ❌
- 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
- 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.
Environment
RN$Bridgeless: true,__turboModuleProxy: false)release-react)getDefaultReactHost(context, packageList, jsBundleFilePath = CodePush.getJSBundleFile()), release build withminifyEnabled falseSummary
In a JS bundle loaded via CodePush (from the CodePush file path),
awaiting a promise returned by a codegen TurboModule method resolvesundefined— the await resolves at the right time, but the value is lost. Consuming the same promise with a plain.thencallback 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) resolvedundefinedfor 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)
checkNotifications()resolves'granted'; CodePush restart applies the update seconds later (same PID, new JS instance) → same call resolvesundefined.--minify false)release-reactbundles fail identically; Metro module resolution is byte-equivalent per sourcemaps; the failing bundle's code is correct on inspection.awaits the native promise also getsundefined). Re-wrapping via.thendoes fix it: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
.thencallbacks and re-wraps them innew 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.