In a React Native Turbo Native Module spec, what is the difference between TurboModuleRegistry.getEnforcing and TurboModuleRegistry.get, and when should you use get?
answer
- one throws, one returns null
- looked up when the spec is imported
- falls back to legacy NativeModules
- optional or platform-only modules
- callers must null-check with get
basics
~20 sgetEnforcing 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 sBoth 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 linesimport 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
Know that getEnforcing throws if the native module is missing while get returns null, and that most modules use getEnforcing.
Explain the shared lookup order, the exact failure of each function, and why the error appears when the spec file is imported.
Choose deliberately: getEnforcing for required modules so misregistration fails early, get only for platform-only or optional modules with explicit null handling.
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