Skip to content

iOS: emitOnBrownfieldMessage: can crash with std::bad_function_call when native posts before JS requires the TurboModule (cold-start race) #420

Description

@jakegriffey

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 BrownfieldMessageToJSNotificationhandleNativeToJSMessage: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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions