ReactNativeBrownfieldModule.mm's +emitMessageFromNative: only checks that the Obj-C singleton exists before calling emitOnBrownfieldMessage::
+ (void)emitMessageFromNative:(NSString *)message {
if (_sharedInstance) {
[_sharedInstance emitOnBrownfieldMessage:@{ @"text": message }];
} else {
NSLog(@"ReactNativeBrownfieldModule is not initialized, dropping message");
}
}
_sharedInstance is set in -init, which runs as soon as the bridge/TurboModuleManager instantiates the Obj-C module. But the codegen SpecBase's _eventEmitterCallback (the std::function that emitOnBrownfieldMessage: invokes) is only populated inside the C++ SpecJSI constructor, which only runs when getTurboModule: fires — i.e. when JS actually evaluates TurboModuleRegistry.getEnforcing('ReactNativeBrownfield').
If a host app calls ReactNativeBrownfield.shared.postMessage(...) (→ posts BrownfieldMessageToJSNotification → handleNativeToJSMessage: → emitMessageFromNative:) after the Obj-C module exists but before JS has required the module — e.g. right as a hosting view controller's viewDidAppear fires on a cold app launch, racing bundle evaluation — _eventEmitterCallback is still empty, and calling it throws std::bad_function_call, crashing the whole app (uncaught C++ exception).
Confirmed still present on main (4.0.0) as of 2026-07-06 — emitMessageFromNative:'s guard is unchanged from what's below (also reproduced against 3.3.0 in our own app):
- (void)handleNativeToJSMessage:(NSNotification *)notification {
NSString *message = notification.userInfo[@"message"];
if (message) {
[ReactNativeBrownfieldModule emitMessageFromNative:message];
}
}
Suggested fixes
- Wrap the
_eventEmitterCallback invocation in a readiness check (or try/catch) inside the generated emitOnBrownfieldMessage:, or
- Expose a callback/promise so host apps can gate native→JS
postMessage calls on "TurboModule ready" rather than just "bundle loaded" (the closest signal startReactNative(onBundleLoaded:) currently offers).
Repro
Timing-dependent — we haven't produced a deterministic repro case, but the code path is unambiguous from source, and we hit it in production (Crashlytics, std::__1::bad_function_call: std::exception, 100% of occurrences within the first second of a session). Happy to share a sanitized stack trace if useful.
Environment
@callstack/react-native-brownfield: 3.3.0 (also read against main/4.0.0 source)
- iOS 26.x, React Native new architecture (TurboModules/Fabric)
ReactNativeBrownfieldModule.mm's+emitMessageFromNative:only checks that the Obj-C singleton exists before callingemitOnBrownfieldMessage::_sharedInstanceis set in-init, which runs as soon as the bridge/TurboModuleManager instantiates the Obj-C module. But the codegenSpecBase's_eventEmitterCallback(thestd::functionthatemitOnBrownfieldMessage:invokes) is only populated inside the C++SpecJSIconstructor, which only runs whengetTurboModule:fires — i.e. when JS actually evaluatesTurboModuleRegistry.getEnforcing('ReactNativeBrownfield').If a host app calls
ReactNativeBrownfield.shared.postMessage(...)(→ postsBrownfieldMessageToJSNotification→handleNativeToJSMessage:→emitMessageFromNative:) after the Obj-C module exists but before JS has required the module — e.g. right as a hosting view controller'sviewDidAppearfires on a cold app launch, racing bundle evaluation —_eventEmitterCallbackis still empty, and calling it throwsstd::bad_function_call, crashing the whole app (uncaught C++ exception).Confirmed still present on
main(4.0.0) as of 2026-07-06 —emitMessageFromNative:'s guard is unchanged from what's below (also reproduced against 3.3.0 in our own app):Suggested fixes
_eventEmitterCallbackinvocation in a readiness check (or try/catch) inside the generatedemitOnBrownfieldMessage:, orpostMessagecalls on "TurboModule ready" rather than just "bundle loaded" (the closest signalstartReactNative(onBundleLoaded:)currently offers).Repro
Timing-dependent — we haven't produced a deterministic repro case, but the code path is unambiguous from source, and we hit it in production (Crashlytics,
std::__1::bad_function_call: std::exception, 100% of occurrences within the first second of a session). Happy to share a sanitized stack trace if useful.Environment
@callstack/react-native-brownfield: 3.3.0 (also read againstmain/4.0.0 source)