Skip to content

iOS: getTag() invokes its callback twice when no session is active, fatal under the New Architecture #833

Description

@Bram-dc

Summary

On iOS, getTag() invokes its RCTResponseSenderBlock twice when there is no active session. Under the New Architecture that is a fatal glog CHECK in React Native itself, so the app dies with SIGABRT instead of the promise rejecting.

Where

ios/NfcManager.m, in getTag: (line 430 on master, line 416 in 3.17.2). The no-session branch calls the callback but does not return, so execution falls through to the final callback(@[[NSNull null], rnTag]);:

    } else {
        callback(@[@"No session available", [NSNull null]]);   // first invoke, no return
    }

    if (ndefTag) {                                             // nil here, so skipped
        ...
        return;
    }

    callback(@[[NSNull null], rnTag]);                         // second invoke

getTag: is the only exported method in the file with this fall-through. Every other method terminates its branches.

Why it is fatal now

React Native wraps callback arguments in convertJSIFunctionToCallback (ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm):

return ^(NSArray *args) {
    if (!callback) {
        LOG(FATAL) << "Callback arg cannot be called more than once";
    }
    ...
};

That is unconditional in Debug and Release. On the old bridge the same double invoke was only a warning, which is why this has shipped for years without being noticed.

How to reproduce

Deterministic, no NFC hardware needed (an iOS simulator is enough, since the buggy branch is reached whenever tagSession/sessionEx is nil):

await NfcManager.getTag()   // with no requestTechnology() beforehand

The app aborts. With a debugger attached the stop is SIGABRT on the com.meta.react.turbomodulemanager.queue thread.

The real-world path is a race on a physical device: requestTechnology(NfcTech.Ndef) resolves when a tag is detected, and if the session is invalidated before getTag() reaches native code (user taps Cancel on the system NFC sheet, CoreNFC times out, tag leaves the field), didInvalidateWithError calls [self reset], which nils the session, and getTag: takes the buggy branch. We see this in production crash reports across several app versions.

Fix

Return after the error callback so every path invokes it exactly once:

    } else {
        callback(@[@"No session available", [NSNull null]]);
        return;
    }

Callers then get a normal getTag() rejection with "No session available" and can recover.

Versions

  • react-native-nfc-manager 3.17.2 (also present on master, so the 4.0.0 beta line has it too)
  • react-native 0.86.2, New Architecture
  • iOS 26.5

Happy to send a PR if that helps.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions