Skip to main content

React Native Bridgeless Mode: Solving 'TurboModuleRegistry.getEnforcing' Failures

 You have flipped the switch. You enabled newArchEnabled in your gradle.properties and Podfile, perhaps even set bridgelessEnabled to true in your Expo config. The build succeeds, but the moment the app launches, it crashes with a red screen (or silent native crash) pointing to:

Uncaught Error: TurboModuleRegistry.getEnforcing(...): 'X' could not be found.

This is the most common blockade when migrating to React Native 0.74+ and the New Architecture. It indicates a severance between your JavaScript Interface (JSI) expectations and the underlying native bindings.

The Root Cause: Synchronous JSI vs. Asynchronous Bridge

To fix this, you must understand the architectural shift.

In the Old ArchitectureNativeModules.MyModule was a proxy object. If the native module failed to load, the proxy might just be empty, or calls would fail silently across the asynchronous bridge.

In the New Architecture (Bridgeless), we use TurboModules. These are loaded via JSI (JavaScript Interface). When your JavaScript code calls TurboModuleRegistry.getEnforcing('MyModule'), it is asking the C++ layer for a synchronous, direct reference to a host object.

The error occurs because:

  1. Codegen Mismatch: The JavaScript spec expects a TurboModule, but the native side hasn't generated or registered the C++ header files.
  2. Interop Failure: The module is legacy (Java/Obj-C only), and the React Native Interop Layer—which wraps legacy modules in a TurboModule shell—failed to auto-detect it.
  3. Registry Lookup: getEnforcing is strict. Unlike get (which returns nullable), getEnforcing throws immediately if the module isn't in the map.

The Fix: Explicit Interop and Codegen Specification

We will solve this in two steps. First, we force the Interop Layer to recognize third-party legacy libraries. Second, we migrate a custom module to strictly satisfy the getEnforcing contract using TypeScript specifications.

Solution 1: Forcing Legacy Libraries into the Interop Layer

If the crash stems from a third-party library (e.g., an analytics or storage SDK) that hasn't officially migrated, you must explicitly register it in the Interop Layer via react-native.config.js.

Create or update react-native.config.js at your project root:

module.exports = {
  project: {
    ios: {
      automaticPodsInstallation: true,
    },
    android: {},
  },
  // Add this dependencies section
  dependencies: {
    'legacy-library-causing-crash': {
      platforms: {
        android: {
          // Force the library to be treated as a legacy module wrapped by the Interop layer
          componentDescriptors: null,
          cmakeListsPath: null,
        },
        ios: {},
      },
    },
  },
};

Note: In rare cases where autolinking fails entirely for Bridgeless, you may need to patch the library's podspec or build.gradle to ensure it is included in the build graph before the Interop layer can see it.

Solution 2: Migrating Custom Modules (The Permanent Fix)

If the crashing module is your own, you must implement the TurboModule Spec. This ensures TurboModuleRegistry.getEnforcing finds the generated C++ bindings.

Step 1: Define the TypeScript Specification

Create a file named NativeBiometricAuth.ts. The naming convention Native<Name>.ts is critical for Codegen discovery.

import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  // Define strict types for your methods
  authenticate(reason: string): Promise<boolean>;
  checkAvailability(): boolean;
  
  // Events are handled differently in TurboModules, 
  // but for strict invocation, we focus on methods.
}

// 1. We attempt to get the module strictly.
// 2. If this fails, the app crashes, alerting us to a native binding issue immediately.
export default TurboModuleRegistry.getEnforcing<Spec>('BiometricAuth');

Step 2: Configure Codegen

In your package.json, configure the Codegen to parse this file.

{
  "name": "my-app",
  "dependencies": {
    "react-native": "0.76.1"
  },
  "codegenConfig": {
    "name": "AppSpecs",
    "type": "modules",
    "jsSrcsDir": "src/specs",
    "android": {
      "javaPackageName": "com.myapp.specs"
    }
  }
}

Run npx expo prebuild --clean (or pod install) to trigger the generation of C++ scaffolding.

Step 3: Connect Native Implementation (iOS Example)

The error usually persists because the native class doesn't conform to the generated protocol. You must modify your Objective-C++ header.

File: ios/BiometricAuth.h

#import <Foundation/Foundation.h>
#import <AppSpecs/AppSpecs.h> // Import the generated header

// The class must conform to the generated spec protocol
@interface BiometricAuth : NSObject <NativeBiometricAuthSpec>
@end

File: ios/BiometricAuth.mm

#import "BiometricAuth.h"

@implementation BiometricAuth

RCT_EXPORT_MODULE()

// The method signature must match the TypeScript Spec exactly
- (void)authenticate:(NSString *)reason 
             resolve:(RCTPromiseResolveBlock)resolve 
              reject:(RCTPromiseRejectBlock)reject {
    // Logic here...
    BOOL success = YES;
    resolve(@(success));
}

// Sync method implementation
- (NSNumber *)checkAvailability {
    return @(YES);
}

// Required for TurboModules to work with the compiled spec
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params {
    return std::make_shared<facebook::react::NativeBiometricAuthSpecJSI>(params);
}

@end

Why This Works

The crash occurs because getEnforcing performs a lookup in the TurboModule C++ registry.

  1. By defining Native<Name>.ts: You provide the source of truth for Codegen.
  2. By running Prebuild/Pod Install: React Native generates a C++ header (AppSpecs.h) that defines the JSI interface.
  3. By implementing getTurboModule in Objective-C++: You physically link your native class instance to that JSI interface.

When JavaScript executes TurboModuleRegistry.getEnforcing('BiometricAuth'), the runtime now finds the registered JSI binding created in the getTurboModule method, preventing the crash.

Conclusion

The transition to Bridgeless mode removes the runtime safety net of the asynchronous bridge. TurboModuleRegistry.getEnforcing is designed to fail hard and fast when native bindings are missing.

To solve these crashes, stop treating native modules as "magic strings" looked up at runtime. Either explicitly register legacy modules in the Interop layer via config or, ideally, migrate your custom modules to strict TurboModule specifications to leverage the full performance benefits of the JSI.