Skip to content

React Native: TurboModuleRegistry.getEnforcing Not Found — Fix

Fix "TurboModuleRegistry.getEnforcing(...): 'X' could not be found" in React Native — from a skipped rebuild to duplicate react-native copies in a monorepo.

react-native react-native-errors turbomodules new-architecture autolinking hermes android ios expo
Bharath G
Reading Progress

On This Page

1. The Error

The most common form, thrown the instant a JS module tries to pull a native module out of the registry:

ERROR  Invariant Violation: TurboModuleRegistry.getEnforcing(...): 'RNCAsyncStorage' could not be found.
Verify that a module by this name is registered in the native binary.
Bridgeless mode: true. TurboModule interop: false.
Modules loaded: {"NativeModules":["PlatformConstants","LogBox","Timing","AppState","SourceCode",
"DeviceInfo","Networking","ImageLoader","LinkingManager","UIManager","DevSettings","DevLoadingView"]}, js engine: hermes

This error is located at:
    in AsyncStorageDemo
    in RCTView
    in App
    in RCTView
    in main(RootComponent)

Call Stack
  invariant                          InitializeCore.js:63:18
  getEnforcing                       TurboModuleRegistry.js:52:10
  <anonymous>                        NativeAsyncStorage.js:9:41
  loadModuleImplementation           require.js:227:6
  guardedLoadModule                  require.js:181:45
  metroRequire                       require.js:105:12

The same failure, but for a module that ships its own friendlier wrapper (React Native Google Sign-In, react-native-webview, and most community libraries do this):

Invariant Violation: "@react-native-google-signin/google-signin" doesn't seem to be linked. Make sure:

- You rebuilt the app after installing the package (npx expo prebuild / pod install / ./gradlew clean)
- You are not using Expo Go — this library ships native code
- You are not running the app from an old build cached on the device or simulator

Pre-TurboModule interop layer (New Architecture disabled, or a library still reading the legacy bridge directly), the exact same missing-native-code cause shows up with different wording — no invariant, just an undefined property access:

TypeError: null is not an object (evaluating '_reactNativeAsyncStorage.default.getItem')

And the native-side symptom when the missing module is one the app needs synchronously at startup — PlatformConstants, DeviceInfo, or similar — is a crash before any red box even renders. On Android, logcat shows:

FATAL EXCEPTION: mqt_native_modules
java.lang.RuntimeException: Failed to call function, expected sender to be instance of: com.facebook.react.turbomodule.core.interfaces.TurboModule
    at com.facebook.jni.NativeRunnable.run(Native Method)
    at com.facebook.react.turbomodule.core.TurboModuleManager.getModule(TurboModuleManager.java)

On iOS it's an NSException out of RCTFatal, with a Hermes stack that has no source-mapped frames in a release IPA — just addresses — unless you've uploaded the .map file to your crash reporter.

Applies to: RN 0.68+ (TurboModuleRegistry has existed since the New Architecture's early rollout), but the exact wording changed twice — the Bridgeless mode: / TurboModule interop: suffix was added once bridgeless became the default (0.76), and from 0.82 (New Architecture only) there is no legacy bridge left to fall back to, so the null is not an object variant disappears for anyone on a current release. Expo Go can never satisfy this for a library with custom native code — Expo Go only ships the fixed set of modules bundled into the Expo Go binary itself.

2. How to Reproduce It

Bare RN CLI, the ordinary "installed a native package, forgot the rebuild" case:

npx @react-native-community/cli@latest init TurboRepro --version 0.87.0
cd TurboRepro
npm install @react-native-async-storage/async-storage@2.2.0

package.json dependency block:

{
  "dependencies": {
    "react": "19.2.0",
    "react-native": "0.87.0",
    "@react-native-async-storage/async-storage": "2.2.0"
  }
}

App.tsx:

import React, {useEffect} from 'react';
import {View, Text} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

export default function App() {
  useEffect(() => {
    AsyncStorage.getItem('token').then(v => console.log('token:', v));
  }, []);

  return (
    <View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
      <Text>Turbo repro</Text>
    </View>
  );
}

Now run it without the native install step:

npx react-native run-android         # Android: JS bundle rebuilds, native APK does not
cd ios && pod install                # <- if you skip this line on iOS the pod graph never sees the module
cd .. && npx react-native run-ios

On Android the error fires the moment getItem is called, because Metro happily bundled the JS half of the package but the previously-built APK's autolinked PackageList.java was never regenerated. On iOS, skip pod install after adding the dependency and the same thing happens — the Xcode project has no RNCAsyncStorage target, so nothing registers modulesProvider["RNCAsyncStorage"].

The monorepo variant — the one that produces the confusing "built-in module PlatformConstants could not be found" — needs a duplicated react-native copy:

repo/
├── package.json                 # workspaces: ["apps/*", "packages/*"]
├── apps/
│   └── mobile/
│       ├── package.json         # "react-native": "0.87.0"
│       └── node_modules/
│           └── react-native/    # <- hoisting failed, second copy landed here
└── packages/
    └── ui/
        ├── package.json         # "react-native": "0.86.2"  (mismatched range)
        └── index.tsx
# from apps/mobile
npm ls react-native
# react-native@0.87.0
# └─┬ (some-lib)
#   └── react-native@0.86.2 extraneous

Metro resolves react-native for most files to the hoisted 0.87.0 copy, but for anything under packages/ui it may resolve the nested 0.86.2 copy instead (different TurboModuleRegistry.js, different Codegen-generated schema hash). The native binary was compiled against one schema; the JS half loaded at runtime is asking with the other. Result: PlatformConstants, a module that is unquestionably registered, "could not be found."

Triggers worth knowing: this reproduces on both platforms, in both debug and release, and is unrelated to Fabric/legacy view rendering — it is purely a JS-to-native module lookup problem. It does not reproduce in Expo Go for any custom native module, ever, by design; it also won't reproduce for a first-party module you already had in the last build you installed on the device — only for anything added or changed since that build.

3. Version Behaviour Matrix

VersionBehavior
0.79Legacy Architecture still selectable. Message includes Bridgeless mode: false when Legacy Architecture is on, true when New Architecture is on (opt-in). Same root causes apply on either.
0.80React 19.1. Deep-import deprecation warnings begin (unrelated to this error, but the two are often hit in the same upgrade PR). No change to TurboModuleRegistry behavior.
0.81Community-maintained JavaScriptCore introduced; on JSC the Hermes-specific stack-trace formatting disappears but the invariant text is identical. Experimental precompiled iOS builds start appearing in more starter templates, which is itself a new way to hit this error (see below).
0.82New Architecture only — the Legacy Architecture flag is gone. The null is not an object (evaluating '_reactNativeX.default...') legacy-bridge wording can no longer occur on a project running current React Native; every occurrence from here on is the Invariant Violation: TurboModuleRegistry.getEnforcing form.
0.83No user-facing breaking changes to module registration. React 19.2 lands; unrelated.
0.84Legacy Architecture components fully removed from core. Precompiled iOS binaries become the default, which means a pod install after bumping react-native itself pulls a prebuilt React-Core.xcframework rather than compiling from source — a stale CocoaPods cache here produces the exact same "could not be found" symptom with a different root cause (mismatched precompiled binary vs. Podfile.lock).
0.85Jest preset moves to @react-native/jest-preset as its own package — an unrelated but frequently co-occurring error in the same upgrade. No TurboModuleRegistry change.
0.86No user-facing breaking changes.
0.87useTurboModules feature flag removed — TurboModules are always enabled, no opt-out exists any more, so this failure mode is now universal rather than New-Architecture-only. Strict TypeScript API becomes default, which can turn a previously-silent NativeModules.RNCAsyncStorage deep import into a type error at compile time, catching some of these before they ever run.

Framework layer: Expo SDK 55–57 (RN 0.83–0.86) and RN 0.87 itself all funnel custom native modules through the identical Codegen/autolinking pipeline — the only Expo-specific wrinkle is that npx expo prebuild regenerates the ios//android/ folders from app.json/config plugins, so a module missing from a config plugin reproduces this error even when npm install and a native rebuild both succeeded.

4. Why It Happens — Surface Level

The native binary currently installed on your device or simulator does not contain code for the module the JS side is asking for, by name, right now. That happens for one of three reasons: you added or updated a package with native code and didn't rebuild the native app afterward; you're running Expo Go, which is a fixed binary that only contains Expo's own module set; or two different copies of react-native (or the library itself) are in play, so the JS half and the native half were compiled against different schemas.

5. Why It Happens — Under the Hood

Autolinking is a build-time step, not a runtime one. When you run pod install or a Gradle sync, the React Native CLI's autolinking package walks node_modules, reads each dependency's react-native.config.js (or infers it from package.json), and generates a manifest — PackageList.java on Android, a generated Swift/Objective-C registration file on iOS — that lists every native package to compile into the app. Metro, by contrast, re-bundles JS on every reload, instantly. Change the JS side and it's live in seconds; change the native side and nothing happens until you rebuild. This asymmetry is the entire bug class: npm install updates the dependency the JS bundler sees immediately, but the native binary keeps running whatever autolinking manifest was baked in at the last build.

TurboModuleRegistry.getEnforcing('RNCAsyncStorage') is a JSI call, not a bridge round-trip. Under the old bridge, NativeModules.RNCAsyncStorage was just a plain JS object populated eagerly at startup from whatever the native side happened to send over — missing modules silently came back undefined, which is why the legacy failure mode was a deferred TypeError: null is not an object at first use, not at lookup. TurboModules deliberately fail fast instead: getEnforcing calls into a native TurboModuleManager (Android) or RCTTurboModuleManager (iOS) that looks the name up in the map built from getReactModuleInfoProvider() / the modulesProvider dictionary Codegen generated from your codegenConfig in package.json. If the name isn't in that map, the native side returns nullptr, and getEnforcing throws immediately, by design, rather than let a nonexistent module silently degrade into confusing behavior three screens later.

Codegen is the piece that ties a TypeScript (or Flow) spec file to that map. It reads specs/NativeX.ts, produces a Java/Kotlin interface on Android and an Objective-C++ protocol on iOS, and both are compiled into the app binary as part of the normal Gradle/Xcode build — ./gradlew generateCodegenArtifactsFromSchema runs automatically as an input to assembleDebug/assembleRelease. Two copies of react-native in a monorepo mean two copies of TurboModuleRegistry.js and, more importantly, potentially two different Codegen schema versions. If the JS engine ends up executing the copy of TurboModuleRegistry.js that a hoisting quirk placed under packages/ui/node_modules/react-native, its internal machinery is still perfectly correct — but the running native binary was compiled from the app's own node_modules/react-native schema. The names usually still match for core modules, so it's not a total mismatch; it's usually a native binary that's simply older than the JS bundle asking it a question it hasn't been told to answer yet, which is functionally identical to "forgot to rebuild," just harder to spot because nothing about the dependency looks stale.

Evidence worth pulling before you guess:

npm ls react-native
npx react-native info

npx react-native info prints the installed React Native version, the Node/JDK/Xcode toolchain, and — critically — whether it can find a single resolved react-native version; a monorepo mismatch shows up here as a version that doesn't match what's in your app's package.json.

6. The Fix

Case 1 — you installed or updated a native package and didn't rebuild. This is the fix for the overwhelming majority of reports:

  npm install @react-native-async-storage/async-storage
+ cd ios && pod install && cd ..
+ npx react-native run-android
+ npx react-native run-ios

A JS-only reload (r r in Metro, Fast Refresh, npx react-native start) never rebuilds native code — you must relaunch through run-android/run-ios, or rebuild in Xcode/Android Studio directly.

Case 2 — you're on Expo Go and the library has native code. Expo Go cannot ever satisfy this; the fix is a development build, not a workaround:

npx expo install expo-dev-client
npx expo prebuild
npx expo run:ios      # or: eas build --profile development

Case 3 — stale cache or a genuinely corrupted native install (common right after an OS/Xcode/AGP upgrade, or after 0.84's move to precompiled iOS binaries where a half-updated Podfile.lock mixes source and prebuilt frameworks):

watchman watch-del-all
rm -rf node_modules && npm install
npx react-native start --reset-cache
cd ios && rm -rf Pods Podfile.lock build && pod install --repo-update && cd ..
cd android && ./gradlew clean && cd ..
rm -rf ~/Library/Developer/Xcode/DerivedData/*

Every line here removes one specific place a stale artifact can hide: Watchman's file-watch state, node_modules (in case a postinstall autolinking hook ran against an old tree), Metro's transform cache, CocoaPods' resolved graph and its intermediate build products, Gradle's build outputs, and Xcode's DerivedData. Running all of them "just in case" without knowing which one matched your symptom is a five-minute tax — worth it after a toolchain upgrade, wasteful as a first response to every red box.

Case 4 — duplicate react-native in a monorepo. Don't paper over it by switching getEnforcing to get() and null-checking — that hides a real version-skew bug behind a silently disabled feature. Fix the duplication instead:

// package.json at the workspace root
{
  "pnpm": {
    "overrides": {
      "react-native": "0.87.0",
      "react": "19.2.0"
    }
  }
}

or, with npm/yarn workspaces, an equivalent resolutions/overrides block, then confirm with npm ls react-native that exactly one copy remains before rebuilding both platforms.

Case 5 — you wrote the TurboModule yourself and it was never registered. Check, in order: the codegenConfig block in package.json matches your specs/ folder; the generated *Package.java is actually added to getPackages() in MainApplication.kt; and, on iOS, modulesProvider in codegenConfig.ios points at the right class name. A spec file with a typo'd method signature fails Codegen loudly at build time (generateCodegenArtifactsFromSchema FAILED) — a module that builds fine but still isn't found almost always means the package-registration step, not the spec, is missing.

7. Best Practices & The Better Design

Scaffold new native modules with create-react-native-library instead of hand-rolling the spec/package/registration wiring — it generates a working react-native.config.js, codegenConfig, and both platforms' registration boilerplate together, so the three pieces that must all agree can't silently drift apart:

npx create-react-native-library@latest react-native-my-module

For genuinely optional native functionality (an analytics SDK you only want on some build flavors, say), use TurboModuleRegistry.get() deliberately and design the fallback, rather than reaching for it as a panic fix for case 4 above:

import {TurboModuleRegistry} from 'react-native';
import type {Spec} from './NativeOptionalAnalytics';

const OptionalAnalytics = TurboModuleRegistry.get<Spec>('OptionalAnalytics');

export function track(event: string) {
  OptionalAnalytics?.track(event) ?? console.log('[analytics stub]', event);
}

In a monorepo, adopt a version-catalogue tool (@rnx-kit/align-deps) so react, react-native, and Reanimated/Screens/Gesture-Handler stay pinned to one resolved version across every workspace package instead of drifting per-package.

8. How to Prevent It Long-Term

Run npx react-native doctor (or npx expo-doctor on Expo) and npm ls react react-native in CI, not just locally, so a duplicate copy fails the build instead of showing up as a device-only crash report. Build a release artifact on both platforms in CI on every PR that touches package.json — a debug-only CI matrix will happily pass while a precompiled-binary/Podfile.lock mismatch (0.84+) only shows up in release. Treat every native dependency bump as requiring the platform-specific rebuild step in the same PR, and add a pre-commit or CI check that fails if Podfile.lock/android/app/build/generated is older than the last native-package change to package.json. Pin dependency ranges with a lockfile and scope Renovate/Dependabot to the RN, Expo, and native-library version set together so they bump in one coordinated PR rather than drifting independently. This connects directly to the clean-rebuild and monorepo-dedupe fixes above — the same npm ls react-native check that diagnoses the bug is the one that should gate your CI so it never reaches a device.

9. Key Takeaways

  • TurboModuleRegistry.getEnforcing throwing immediately (instead of the old bridge's deferred undefined) is intentional fail-fast design, not a regression — the fix is almost always "the native binary is older than the JS bundle," not a code bug.
  • Autolinking and Codegen are build-time steps; npm install alone never rebuilds the native app on either platform — pod install plus a real run-ios/run-android (or Xcode/Android Studio rebuild) is required after any native dependency change.
  • Expo Go cannot run any library with custom native code, ever — the fix there is a development build, not a longer troubleshooting session.
  • A "core" module like PlatformConstants failing to resolve almost always means two copies of react-native are in play in a monorepo, not that React Native itself is broken — confirm with npm ls react-native before touching native code.
  • Reach for TurboModuleRegistry.get() only when a module is meant to be optional by design; using it to silence this error elsewhere just defers a real version-skew bug to a harder-to-debug place.
react-nativereact-native-errorsturbomodulesnew-architectureautolinkinghermesandroidiosexpo

From aspiring developer to blogger, I test learning platforms, simplify programming syntax, and share resources that work. Helping you code smarter as I grow myself. New or experienced, you're welcome here.

Comments