After a teammate adds a native library, your Expo development build fails with "Cannot find native module"; how do you diagnose it and stop it recurring?
answer
- JavaScript is newer than the binary
- Metro reload cannot add native code
- rebuild, or regenerate then rebuild
- fingerprint of native inputs
- requireOptionalNativeModule for soft features
basics
~20 sThe installed development build predates the library, so its JavaScript asks for native code the binary lacks. Rebuild the development build, and stop recurrences by detecting native changes, for example with a fingerprint check that says when a new build is needed.
solid answer
~40 s`Cannot find native module 'X'` is thrown by Expo's `requireNativeModule` when the running binary has no module with that name. After a teammate adds a native library, Metro serves the new JavaScript immediately, but your installed development build was compiled before the library existed. Confirm it: the library has native code, and your build is older than the commit that added it. The fix is a new development build; if you build locally from existing native folders, run `npx expo prebuild` first so the library's plugin is applied. To stop it recurring, make native changes visible: compute a fingerprint of the native inputs (for example with `@expo/fingerprint`) in CI, and publish a fresh shared development build and alert the team when it changes. For optional features, `requireOptionalNativeModule` returns `null` instead of throwing.
code
typescript · 10 linesimport { requireOptionalNativeModule } from 'expo';
type HabitWidget = { reloadAll(): void };
// null when the installed binary predates the native module
const widget = requireOptionalNativeModule<HabitWidget>('HabitWidget');
export function refreshWidgets(): void {
widget?.reloadAll();
}go deeper
Recall that native libraries need a new build: a Metro reload cannot add native code to the installed app.
Explain the binary-versus-JavaScript split, and the diagnosis order: native library, build age, wrong client, stale native folder.
Show the prevention: fingerprinting native inputs in CI, rebuilding and announcing shared development builds, and guarding optional native features.
Set the team's policy for native changes: how builds are shared, who triggers rebuilds, and how the same compatibility rule governs over-the-air updates.
## Reading the error In an Expo app, native modules written with the Expo Modules API are looked up at runtime by name. `requireNativeModule('X')` throws **`Cannot find native module 'X'`** when the running binary has no such module; `requireOptionalNativeModule('X')` returns `null` instead. Libraries built on React Native's Turbo Modules fail with their own "could not be found" message, but the cause is the same: the JavaScript expects native code that the installed app does not contain. ## Why it appears right after a teammate's change A development build splits the app in two: - the **binary** on your device, compiled at some point in the past; - the **JavaScript**, served live by Metro from your current checkout. When a teammate merges a library with native code, pulling their commit updates your JavaScript immediately. Your binary is unchanged. The first screen that imports the library asks for its native module and gets nothing. ## Diagnosis in order 1. **Check the library**: does it contain native code (an `ios` or `android` directory, an Expo module config)? Pure JavaScript libraries cannot cause this error. 2. **Check the build's age**: was your development build compiled before the commit that added the library? 3. **Check which client is running**: an app opened in Expo Go instead of the development build shows the same error for any library Expo Go does not bundle. 4. **For local builds, check the native folder**: `npx expo run:ios` does not regenerate an existing `ios/`, so if the library needs a config plugin, the old folder may build without it. 5. **Rebuild and retest**: `npx expo prebuild`, then `npx expo run:ios` / `run:android`, or install the latest shared EAS development build. ## Stopping it recurring The root problem is that native changes are invisible in a JavaScript-first workflow. Make them visible: - **Fingerprint the native inputs**: `@expo/fingerprint` hashes the things that shape the native app (native dependencies, app config, plugins, native folders when present). Run `npx @expo/fingerprint fingerprint:generate` in CI and compare with the fingerprint of the last shared development build. - **Rebuild on change**: when the fingerprint changes, CI produces a new shared development build and posts a message that everyone must install it. - **Say so in the pull request**: a template checkbox for "adds or updates native code" catches what automation misses. - **Pin the build to the branch**: reviewers testing a branch with a native change need that branch's build, not the shared one from `main`. | Approach | Catches | Cost | |---|---|---| | Fingerprint in CI | every native-input change | one CI step, extra builds | | PR checkbox | intent, including edge cases | relies on people | | Always rebuild daily | eventual consistency | build minutes, still a gap during the day | ## Degrading gracefully for optional features When a native feature is optional, such as a haptics or a sharing extension, import it through a guard so an older binary keeps working: - use `requireOptionalNativeModule` from `expo` and hide the feature when it returns `null`; - keep the check at the module boundary, so screens do not each test for the native module. This also protects production: an over-the-air JavaScript update that references a new native module would hit the same error on binaries that lack it, which is why such updates must target only compatible builds. ## What not to do - Clearing the Metro cache: the problem is native, and no bundler setting adds native code. - Catching the error around every call site: it hides a build mismatch that will reappear elsewhere. - Reinstalling `node_modules`: the JavaScript side is already correct. ## A worked timeline 1. Monday: the shared development build is produced from `main`. 2. Tuesday: a teammate merges a native widget module; CI's fingerprint differs from Monday's build. 3. Without a check, developers who pull `main` see `Cannot find native module 'HabitWidget'` on the first screen that imports it. 4. With the check, CI produces a new development build on Tuesday and posts that it must be installed; developers who have not installed it yet still hit the error, but know why. 5. With `requireOptionalNativeModule` around the widget, the rest of the app keeps working on Monday's build until people update. The three layers together, detection, distribution and graceful degradation, turn a confusing crash into a routine update.
- Why does clearing the Metro cache not fix Cannot find native module in an Expo development build?Metro only serves JavaScript. The error means the installed binary lacks the native module, which only a new native build can add. Clearing the cache rebuilds the same JavaScript that asks for the same missing module.
- How does a fingerprint check tell a team when to rebuild development builds?`@expo/fingerprint` hashes the inputs that shape the native app, such as native dependencies, app config and plugins. CI compares the current hash with the one the shared development build was made from; a difference means native code changed and a new build is needed.
saying these in an interview costs you the question
- Clear the Metro cache and the native module will appear
- Reinstall node_modules; the library did not install correctly
- Wrap every call in try/catch and move on
- npx expo run:ios always regenerates native folders, so stale plugins are impossible
- The error means the library is incompatible with Expo