Skip to main content
Only one NFC session can be open on a device at a time. That is a platform rule, not a library one: on Android reader mode is exclusive, and on iOS a second NFCTagReaderSession queues behind the first. So the interesting question is not how to open one — it is how to be certain it closed. A leaked session keeps the iOS scanning sheet up and holds Android’s NFC controller. Neither symptom appears where the leak is. Both appear two screens later, as NFC having “randomly stopped working”.

Three ways in

withTag

One tag, scoped. Reach for this.

openSession

Several tags, your own UI.

onTag

Continuous. Kiosks and doors.

withTag — one tag, and the session closes itself

The session closes on every path out:
The tag is only valid inside the callback. Keeping a reference and using it afterwards rejects with sessionClosed, which is deliberate: the alternative is a native handle that outlives its session and fails somewhere unrelated.

openSession — several tags, with your own UI between them

await using closes the session when the block ends, however it ends. Without explicit resource management in your toolchain, a finally does the same job:
Prefer withTag. Reach for openSession only when you genuinely need to keep the iOS sheet up across several taps — a two-tag pairing flow, or a “now tap the second card” step. Everything else is withTag in a loop, which cannot leak.

onTag — continuous, for a kiosk or a door

onTag returns a Subscription, not a promise, so a failure has nothing to reject into — that is what onError is for. Leaving it out makes a stream that dies silently.
This is really an Android shape. Reader mode stays up indefinitely there. iOS caps a session at 60 seconds with a system sheet on screen for all of it, so the stream ends with sessionTimeout. Restarting is left to you, because on iOS restarting means putting the sheet back up, which is a product decision rather than a technical one.
In a component, use useNfcTagStream: it ties the stream’s lifetime to the mount and to screen focus, which is the part that is easy to get wrong.

Options, in full

Every entry point takes the same ScanOptions.
TagTech[]
required
Which technologies you are willing to accept. A tag carrying none of them is never surfaced. On iOS this also decides the reader session’s polling options.
number
Your own deadline. Rejects with timeout — which is deliberately not sessionTimeout, iOS’s own 60-second cap that no library can extend.
AbortSignal
Rejects with aborted when the signal fires. Aborting before the call reaches the radio does not touch the radio at all.
IosScanOptions
AndroidScanOptions
SessionConfig

Cancelling and timing out

Two deadlines exist and they are not the same thing, which is why they have different codes:
aborted is not userCancelled. The first is your signal; the second is the user dismissing the sheet. Collapsing the two — which the library this one replaces did — makes it impossible for an app to decide whether to show a message.

Availability, before you offer the feature

nfc.capabilities also reports what the platform can do beyond tags: hce, observeMode, pollingFrames, vas, backgroundReading, and tagLost, which says whether tag removal is delivered by the platform or polled for.

Tags that arrive without a session

A tag tapped while your app is closed or backgrounded does not come through reader mode at all — it arrives as an Android intent, and it needs manifest configuration you have to opt into.
Both hand the tag to a callback and release it afterwards, the way withTag does. The full picture, including the Android 17 permission that decides whether the tag arrives at all, is in Background tag reading.

Next

Tags

What you can do with the thing a session hands you.

Error reference

Every code a session can reject with.