skip to content

In a React Native Turbo Native Module spec, what is the difference between TurboModuleRegistry.getEnforcing and TurboModuleRegistry.get, and when should you use get?

level: middleimportance: should knowfreq 38%

answer

  1. one throws, one returns null
  2. looked up when the spec is imported
  3. falls back to legacy NativeModules
  4. optional or platform-only modules
  5. callers must null-check with get

basics

~20 s

getEnforcing returns the module or throws an invariant error saying it could not be found in the native binary; get returns null instead. Use get only when the module may legitimately be missing, and null-check it at every call site.

solid answer

~40 s

Both functions look the module up by name, first through the Turbo Module proxy and then, for compatibility, in the legacy `NativeModules` object. `TurboModuleRegistry.getEnforcing<Spec>('NativeBatteryHealth')` returns a non-null `Spec` or **throws** "'NativeBatteryHealth' could not be found. Verify that a module by this name is registered in the native binary." `TurboModuleRegistry.get<Spec>(...)` returns **`Spec | null`**. Because the spec's default export runs the lookup, a missing module with `getEnforcing` fails **as soon as the spec file is imported**, which surfaces native misconfiguration early and loudly. Use `get` only when absence is a legitimate state, for example a module that exists on one platform, sits behind a build flag, or is provided by an optional dependency, and then handle `null` wherever the module is used.

code

typescript · 9 lines
typescript
import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';

export interface Spec extends TurboModule {
  getBatteryLevel(): number;
}

// Built only into Android flavours; null elsewhere
export default TurboModuleRegistry.get<Spec>('NativeBatteryHealth');

go deeper

for a junior

Know that getEnforcing throws if the native module is missing while get returns null, and that most modules use getEnforcing.

for a middle

Explain the shared lookup order, the exact failure of each function, and why the error appears when the spec file is imported.

for a senior

Choose deliberately: getEnforcing for required modules so misregistration fails early, get only for platform-only or optional modules with explicit null handling.

for a principal

Set a team rule that optional native capabilities are modelled as nullable modules with a single wrapper, so product code never guesses whether a platform feature exists.

## What both functions do `TurboModuleRegistry` is the JavaScript entry point for loading a native module declared by a spec. Both of its lookup functions take the module's registered name and a type argument, and both run the same search: 1. ask the **Turbo Module proxy** that the New Architecture installs in the JavaScript runtime for a module with that name; 2. if that returns nothing, look in the legacy **`NativeModules`** object, so modules served through the interop layer are still found; 3. if both come back empty, the module is missing. The only difference is what happens in step 3. ## The difference | | `getEnforcing<Spec>(name)` | `get<Spec>(name)` | |---|---|---| | Return type | `Spec` | `Spec \| null` | | Module missing | Throws an invariant error | Returns `null` | | Error text | "'<name>' could not be found. Verify that a module by this name is registered in the native binary." | none | | Caller's job | Just call methods | Check for `null` before every use | ## When the lookup happens A spec file's default export is the result of the lookup, for example `export default TurboModuleRegistry.getEnforcing<Spec>('NativeBatteryHealth')`. That line runs **when the spec module is first imported**, not when a method is called. With `getEnforcing`, a missing native module therefore throws while the importing file is being evaluated. In practice this means: - a misregistered module crashes the first screen that imports it, during development, which is where you want to find it; - the error text points at registration, which is the usual cause: a name mismatch, a module not added to its package, or a native build that predates the module. ## When `get` is the right choice Use `get` when the module's absence is a **supported state**, not a bug: - **Platform-only modules**: a battery-health module implemented only on Android, used from shared code on both platforms. - **Optional capabilities**: a feature compiled into some build flavours of a device-management app and not others. - **Optional dependencies**: a library that enhances behaviour when another native package is installed. With `get`, the spec's default export is typed `Spec | null`, and TypeScript forces every caller to handle the `null` case, which makes the optionality explicit in the code. ## When `get` is the wrong choice - **Hiding a real bug**: switching to `get` because `getEnforcing` threw only moves the failure to a later `null` access, further from the cause. - **Required modules**: if the app cannot function without the module, failing loudly at import is better than a feature that quietly does nothing. - **A single call site**: consider whether a platform-suffixed spec file or a platform check around the import would express the intent more clearly. ## How the fallback to NativeModules matters Because both functions check the legacy `NativeModules` object after the Turbo Module proxy, a module registered the old way under the same name also satisfies the lookup. That keeps interop-served modules working with typed specs, but it has a consequence worth knowing: `getEnforcing` succeeding proves only that **some** module answers to that name. It does not prove the module was built from your spec, so a leftover legacy module with a colliding name can mask a missing Turbo Module. Keeping module names unique across the app and its libraries avoids that trap. ## A worked example In a device-management app, battery health is required on the Android devices the fleet uses, but the same JavaScript also runs on iOS, where the module is not built. Two sound designs: 1. **One spec with `get`**, and a small wrapper that returns "unavailable" when the module is `null`, so screens render a clear message on iOS. 2. **A platform-suffixed spec file** (`NativeBatteryHealth.android.ts`) with `getEnforcing`, plus a platform-specific import path in app code. Either is defensible; what is not defensible is `getEnforcing` in shared code for a module one platform never provides. ## Summary `getEnforcing` promises the module exists and throws at import time if it does not; `get` admits it may not and returns `null`. Choose based on whether absence is a bug or a supported state, and let the type system carry that decision to every call site.

  • getEnforcing throws 'could not be found' for a module you just wrote. What do you check?
    That the native side registers the same name as the string in the spec (for example the Android module's `getName()`), that the module is actually added to the package or provider that registers it, and that you rebuilt the native app after adding it; a JavaScript reload alone does not add native code to the binary.
  • Why does TurboModuleRegistry also look in NativeModules?
    For compatibility with modules that are not Turbo Modules. If the Turbo Module proxy returns nothing, the registry checks the legacy `NativeModules` object, which in the New Architecture is served by the interop layer, so a spec can still find a module implemented the old way.

saying these in an interview costs you the question

  • getEnforcing waits until a method is called before failing
  • get and getEnforcing only search Turbo Modules, never NativeModules
  • Switching to get is a good fix when getEnforcing throws
  • get retries the lookup until the native module finishes loading
  • A JavaScript reload makes a newly written native module available