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.
Summary
On iOS,
getTag()invokes itsRCTResponseSenderBlocktwice when there is no active session. Under the New Architecture that is a fatal glogCHECKin React Native itself, so the app dies withSIGABRTinstead of the promise rejecting.Where
ios/NfcManager.m, ingetTag:(line 430 onmaster, line 416 in 3.17.2). The no-session branch calls the callback but does not return, so execution falls through to the finalcallback(@[[NSNull null], rnTag]);: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):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/sessionExis nil):The app aborts. With a debugger attached the stop is
SIGABRTon thecom.meta.react.turbomodulemanager.queuethread.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 beforegetTag()reaches native code (user taps Cancel on the system NFC sheet, CoreNFC times out, tag leaves the field),didInvalidateWithErrorcalls[self reset], which nils the session, andgetTag: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:
Callers then get a normal
getTag()rejection with "No session available" and can recover.Versions
master, so the 4.0.0 beta line has it too)Happy to send a PR if that helps.