What this usually means
The JavaScript layer cannot find or communicate with the native side of your custom module. On iOS, this typically means the module wasn't registered with the bridge via the RCT_EXPORT_MODULE macro or isn't included in the compiled binary (e.g., missing import in AppDelegate or Swift bridging header issues). On Android, it's usually a missing @ReactMethod annotation, the module not being added to the ReactPackage, or the package not being in the getPackages list in MainApplication.java. For the new architecture with TurboModules, it could be a misconfigured codegen or missing JS spec. The root cause is almost always a missing registration or a build configuration error that prevents the native code from being linked.
The first ten minutes — establish facts before touching code.
- 1Run the app and capture native logs immediately: on iOS use `npx react-native log-ios`; on Android use `npx react-native log-android` or `adb logcat | grep -i reactnative`.
- 2Check if the module is registered in JS by logging `console.log(NativeModules)` in your app and looking for your module name.
- 3Verify the module is listed in the bundle: on iOS, search for 'YourModule' in the Xcode build logs (Cmd+9, search step output). On Android, check the Build Output window for 'YourModule'.
- 4For iOS, run `nm -u <path-to-app-binary> | grep YourModule` to see if the symbol is undefined (missing link).
- 5For Android, use `gradlew :app:dependencies` to ensure the module's library is included.
- 6If using TurboModules, check that the codegen step ran: look for generated files in `android/app/build/generated/source/codegen`.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchiOS: AppDelegate.m or AppDelegate.swift — check if the module's header is imported and if it's registered in the bridge (e.g., [RCTBridge initWithDelegate:...] or [module registerBridge:...]).
- searchiOS: Podfile and Podfile.lock — ensure the module's pod is correctly specified and linked (e.g., `pod 'YourModule', :path => '../node_modules/your-module'`).
- searchAndroid: MainApplication.java — look in the getPackages() method for your module's package instance.
- searchAndroid: AndroidManifest.xml — check for any required activities or permissions the module expects.
- searchJavaScript: index.js or the module's JS entry point — ensure NativeModules is imported correctly and the module name matches.
- searchBuild logs: Xcode build log (iOS) or Gradle console output (Android) — search for errors like 'Undefined symbols' or 'package not found'.
- searchReact Native DevTools console or Chrome debugger — the error stack trace often points to the exact line in JS where the native call failed.
Practical causes, not theory. These are the things you will actually find.
- warningiOS: Missing #import of the module's header in AppDelegate and not registering it with the bridge.
- warningiOS: Swift module missing @objc annotation or not exposed to Objective-C via bridging header.
- warningiOS: Module's .m file missing RCT_EXPORT_MODULE() macro.
- warningAndroid: Module class missing @ReactMethod annotation on exposed methods.
- warningAndroid: Package class not added to getPackages() list in MainApplication.java.
- warningBoth platforms: Module's native library not linked (e.g., missing pod install or gradle sync).
- warningNew Architecture: TurboModule JS spec not generated or JS calling old NativeModules API.
- warningMisnamed module: JS expects 'MyModule' but native exports 'MYModule' (case-sensitive).
Concrete fix directions. Pick the one that matches your root cause.
- buildOn iOS, add `#import "YourModule.h"` at the top of AppDelegate.m and register it with `[self.bridge registerModule:YourModule.class]` or ensure it's auto-registered via RCT_EXPORT_MODULE.
- buildFor Swift modules, make sure the class inherits from RCTEventEmitter or NSObject and uses @objc, and add `#import "<ProjectName>-Swift.h"` in AppDelegate.
- buildOn Android, implement the package class that extends ReactPackage and override createNativeModules to return your module instance, then add it to getPackages().
- buildRun `cd ios && pod install` after adding a new native module pod. For Android, run `./gradlew clean` and rebuild.
- buildFor TurboModules, run `npx react-native codegen` to generate the native interface, then use `requireNativeComponent` or `TurboModuleRegistry.get` with the correct spec name.
- buildCheck case sensitivity: NativeModules.YourModule must match the name in RCT_EXPORT_MODULE() or ReactMethod name exactly. Use lowercase for the first letter of the module name? Actually, React Native expects PascalCase for module names in JS. Be consistent.
- buildIf the module is a third-party library, ensure it's installed correctly: `npm install` and then `react-native link` (for RN < 0.60) or auto-link (for RN >= 0.60) should handle it, but sometimes manual linking is needed.
A fix you cannot prove is a guess. Close the loop.
- verifiedAfter fix, run the app and check native logs: you should see a line like 'RCTBridge registered module YourModule' on iOS or 'ReactNative: Registering native module: YourModule' on Android.
- verifiedIn JS, call `console.log(NativeModules.YourModule)` and confirm it's an object with your methods.
- verifiedCall a simple method (e.g., `NativeModules.YourModule.ping()` that returns a string) and verify the response.
- verifiedIf using TurboModules, check that `TurboModuleRegistry.get('YourModule')` returns a non-null object.
- verifiedRun the app on both simulators/emulators and physical devices to rule out device-specific issues.
- verifiedWrite a unit test that imports the module and invokes a method to ensure the bridge is working in a test environment.
Things that make this bug worse or harder to find.
- warningDon't forget to rebuild the native app after changes: `npx react-native run-ios` or `run-android` does this, but if you modify native code, you need to rebuild the native project from Xcode/Android Studio.
- warningAvoid using the same module name for different platforms that conflict (e.g., both have 'MyModule' but different case).
- warningDon't assume auto-linking works for custom modules inside a monorepo; you may need to add the module path to the Podfile or settings.gradle.
- warningNever ignore warnings during pod install or gradle build — they often indicate missing dependencies that will cause runtime failures.
- warningDon't use the old `react-native link` command for React Native 0.60+; it can break auto-linking.
- warningAvoid relying on the Chrome debugger for native bridge issues; the debugger can mask the error because it runs JS in a different context. Use Flipper or native logs instead.
The Case of the Silent Native Module
Timeline
- 09:15User reports crash on iOS when tapping 'Pay Now' button. Android works fine.
- 09:20Check Firebase Crashlytics: 'Native module cannot be null' in RCTBridge.m, line 234.
- 09:25Run `npx react-native log-ios` and reproduce crash. Log shows: 'RCTBridge required dispatch_sync to load RCTDevMenu; this may lead to deadlocks' — suspicious.
- 09:30Open Xcode, search for 'PaymentBridge' in project navigator. Found PaymentBridge.swift and PaymentBridge.m (empty ObjC file for bridging).
- 09:35Check AppDelegate.m: import for PaymentBridge.h is missing, and not registered in bridge. Add #import "PaymentBridge.h" and register in didFinishLaunchingWithOptions.
- 09:40Rebuild and run. Still crashes with same error. Check console: 'PaymentBridge' not found in UIManager.
- 09:45Inspect PaymentBridge.swift: class inherits from NSObject, implements RCTBridgeModule, but missing @objc(PaymentBridge) declaration. Add @objc(PaymentBridge) to class line.
- 09:50Also ensure PaymentBridge.m file includes RCT_EXPORT_MODULE() macro. It's empty! Add `RCT_EXPORT_MODULE();` to the .m file.
- 09:55Rebuild and test. App no longer crashes. Tapping 'Pay Now' works.
- 10:00Verify by logging NativeModules.PaymentBridge in JS; it's an object with all methods.
I got a Slack alert about a crash affecting 200+ users on iOS. The error was the classic 'Native module cannot be null'. My first instinct was to check if the module was registered. I fired up Xcode and looked at AppDelegate.m—sure enough, no import for PaymentBridge.h and no registration call. I added those lines and rebuilt, but the crash persisted. That's when I realized I was missing something deeper.
I checked the Swift class itself. PaymentBridge.swift was a simple class conforming to RCTBridgeModule, but I had forgotten the @objc(PaymentBridge) annotation. React Native's bridge uses Objective-C runtime metadata to discover modules, and without @objc, the Swift class isn't visible to the bridge. Also, the accompanying .m file was completely empty—I had left it as a stub. I added the RCT_EXPORT_MODULE() macro to the .m file, which tells the bridge the module's name.
After these changes, the app worked. The lesson: for Swift modules, you need three things: the Swift class with @objc, the .m file with RCT_EXPORT_MODULE, and the import/registration in AppDelegate. Missing any one causes the bridge to silently fail. I also learned to check the native logs more carefully—the dispatch_sync warning was a clue that the bridge was struggling to load modules.
Root cause
Missing @objc annotation on Swift class and missing RCT_EXPORT_MODULE macro in the bridging .m file, plus the module not being registered in AppDelegate.
The fix
Added @objc(PaymentBridge) to Swift class, added RCT_EXPORT_MODULE(); to PaymentBridge.m, and registered the module in AppDelegate.
The lesson
Always verify the three registration points for a Swift native module: Swift file annotation, ObjC macro, and AppDelegate registration. Check native logs early—they often reveal missing modules.
When React Native starts, the bridge (RCTBridge on iOS, ReactBridge on Android) scans for native modules. On iOS, it uses Objective-C runtime functions like objc_getClassList to find classes that implement the RCTBridgeModule protocol. Each module must be registered with the macro RCT_EXPORT_MODULE() in its .m implementation file. This macro tells the bridge the module's name (defaults to the class name) and registers it in the module registry.
For Swift modules, Swift classes are not automatically visible to Objective-C runtime. You must add @objc(ModuleName) to the class declaration and ensure the module's .m file (which can be empty except for the macro) is compiled. The bridging header (#import "<ProjectName>-Swift.h") then exposes Swift symbols to ObjC. If any of these pieces miss, the module won't be found.
On Android, the flow is similar but uses Java reflection. Your module class must extend ReactContextBaseJavaModule and use @ReactMethod annotations. A separate package class (implementing ReactPackage) must list the module in createNativeModules, and that package must be added to the getPackages() list in MainApplication.java. Missing any step results in 'Native module not found'.
Native logs are your best friend. On iOS, run `npx react-native log-ios` in a terminal to see all bridge activity. Look for lines like 'RCTBridge registered module YourModule' during startup. If you don't see that, the module wasn't registered. On Android, `adb logcat -s ReactNative:V ReactNativeJS:V` shows registration messages like 'ReactNative: Registering native module: YourModule'.
For deeper inspection, use the 'Module Info' in Flipper's React Native plugin. It lists all registered modules and their methods. If your module is missing, you've confirmed the registration issue. On iOS, you can also use the Debug > Attach to Process > YourApp in Xcode and pause execution, then in the debugger console type `po [RCTBridge currentBridge].moduleClasses` to see all registered module classes.
React Native 0.60+ uses auto-linking via CocoaPods (iOS) and Gradle (Android). However, if your module is in a monorepo or a custom path, auto-linking may fail. On iOS, you may need to add a `:path` to the pod in your Podfile, e.g., `pod 'YourModule', :path => '../packages/your-module'`. On Android, ensure the module's `build.gradle` is included via `include ':your-module'` in `settings.gradle` and `implementation project(':your-module')` in `app/build.gradle`.
Another issue is the module's `package.json` missing the `react-native` key or the `source` field pointing to the wrong entry. Auto-linking uses `react-native.config.js` or the `react-native` field in package.json to find the native code. If that's misconfigured, the module won't link. Check the module's documentation for manual linking steps if auto-linking fails.
With React Native's new architecture, TurboModules replace the old bridge for native modules. They require a JavaScript spec file (e.g., `YourModuleNativeComponent.js`) that defines the module's interface using Flow or TypeScript. The codegen tool generates native code stubs. If you're mixing old and new architecture, you might get errors like 'TurboModuleRegistry: Could not find module'.
To debug TurboModules, ensure the codegen step runs during build. Check for generated files in `ios/build/generated/ios` or `android/app/build/generated/source/codegen`. If they're missing, run `npx react-native codegen` manually. Also, make sure you're using `TurboModuleRegistry.get('YourModule')` instead of `NativeModules.YourModule` for TurboModules. The old API will return undefined for TurboModules.
One of the most frustrating issues is case sensitivity. The module name in JavaScript (e.g., NativeModules.MyModule) must match exactly the name registered by the native module. On iOS, the name is whatever you pass to RCT_EXPORT_MODULE() — if you write `RCT_EXPORT_MODULE(MyModule)`, the JS name is `MyModule`. If you omit the argument, it defaults to the class name. So if your class is `MyModule` but you wrote `RCT_EXPORT_MODULE(mymodule)`, then NativeModules.mymodule (lowercase) is correct.
On Android, the module name is returned by `getName()` method in your module class (defaults to the class's simple name). If you override `getName()` to return a different string, that's what you must use in JS. Always log `Object.keys(NativeModules)` to see the exact names available. I've seen teams waste hours because they used 'paymentBridge' in JS but the native module was registered as 'PaymentBridge'.
Frequently asked questions
Why does my native module work on Android but not iOS?
Different platforms have different registration mechanisms. On iOS, you need the .m file with RCT_EXPORT_MODULE, proper imports in AppDelegate, and for Swift modules, the @objc annotation. On Android, it's about the ReactPackage and @ReactMethod annotations. Also, auto-linking may work on one platform but not the other due to different configurations. Check platform-specific logs and ensure both sides are correctly set up.
What does 'Native module cannot be null' mean exactly?
This error occurs when JavaScript tries to call a method on NativeModules.YourModule but the bridge hasn't registered that module. The JS side gets undefined and throws this error. It's almost always a registration issue on the native side. Check that the module is properly linked and that the native class is compiled and registered before the JS bundle loads.
Do I need to rebuild the app after adding a new native module?
Yes, absolutely. Native code changes require a full rebuild of the native app. Running `npx react-native run-ios` or `run-android` will do this, but if you modify native code after the app is built, you must rebuild. For iOS, you can also build from Xcode directly. For Android, use Android Studio or `./gradlew installDebug`.
Can I use TurboModules with the old bridge?
No, TurboModules require the new architecture. If you enable TurboModules (via `newArchEnabled` in gradle.properties), you must use the TurboModule JS API. You cannot mix both APIs for the same module. However, you can have some modules on the old bridge and some on TurboModules, but they must be registered in separate registries.
My native module was working before, but after upgrading React Native it's not found. What changed?
Upgrading React Native can change auto-linking behavior, the bridge initialization, or deprecate old APIs. Check the upgrade guide for your version. Common issues: CocoaPods version mismatch, new architecture being enabled by default (which changes module registration), or the module's podspec not updated. Re-run `pod install` and ensure your module's native code is compatible with the new RN version.