Description
On iOS, Share.share(...) can return a Promise that neither resolves nor rejects when the underlying UIKit presentViewController: call is silently dropped by iOS — specifically when the presenter view controller's view is not currently in the window hierarchy. The share dialog never appears, the JS Promise stays pending forever, and the caller's try { await Share.share(...) } catch cannot recover the call.
This is a contract violation: the Promise returned by Share.share is documented to either resolve with { action, activityType } or reject with an error. There is currently a third outcome — "neither" — that any caller awaiting that Promise has no way to handle.
Looking at React/CoreModules/RCTActionSheetManager.mm (showShareActionSheetWithOptions), the iOS native implementation:
- Picks the topmost presented controller via
RCTPresentedViewController().
- Sets
shareController.completionWithItemsHandler (which is the only path that ever invokes failureCallback or successCallback, i.e. the only path that settles the JS Promise).
- Calls
[parentViewController presentViewController:alertController animated:YES completion:nil] with no completion: block and no validation of the presenter.
When iOS rejects the present, it does so silently: a warning is written to the system log, but the share controller is never presented, so completionWithItemsHandler is never called. There is no other path back to JS — the Promise leaks forever.
Conceptual code that triggers the bug
// Any navigation transition that causes the topmost iOS view controller to
// change. Could be a modal dismiss, a stack pop, a tab change — anything
// that triggers react-native-screens (or another library) to detach and
// reattach an underlying RNSScreen view on the next UIKit layout pass.
navigation.goBack()
// Issued in the same JS task as the navigation above, this Share.share
// call lands on the main queue before the layout pass that reattaches
// the underlying screen's view to the window hierarchy.
// UIKit silently drops presentViewController:, completionWithItemsHandler
// is never invoked, and the Promise below neither resolves nor rejects.
try {
await Share.share({ url: 'https://example.com' })
console.log('share finished') // never logged
} catch (e) {
console.log('share rejected', e) // also never logged
}
In practice this happens deterministically in production apps under realistic main-thread load. Inserting an iOS-only await new Promise(r => setTimeout(r, 300)) between the navigation call and Share.share reliably masks the issue, which is independent confirmation that the cause is a main-thread timing race rather than something specific to the call site.
Note on the linked reproducer: the snippet shows the call pattern but does not reliably reproduce the bug in isolation. The race only fires under realistic main-thread contention (heavy view trees, animations, native modules running on the main queue), which is a known property of UIKit "host not in window hierarchy" races. We attempted several standalone variants — modal / formSheet / transparentModal / native-stack push, with and without an artificial heavy WebView + Animated workload — and none fired the race in a clean app. The device log section below is the strongest evidence of the actual failure.
Native log evidence
Captured via xcrun simctl spawn booted log stream --predicate 'eventMessage CONTAINS "Attempt to present" OR eventMessage CONTAINS "UIActivityViewController"' while reproducing the bug:
(ShareSheet) UIActivityViewController: initialized with activityItems (
"https://example.com"
)
(ShareSheet) _UIActivityViewControllerPresentationController: initialized with presentedVC:<UIActivityViewController: 0x...> presentingVC:(null)
(UIKitCore) [com.apple.UIKit:Presentation] Attempt to present <UIActivityViewController: 0x...> on <RNSScreen: 0x...> (from <RNSScreen: 0x...>) whose view is not in the window hierarchy.
The last line is iOS explicitly refusing the presentation. No further calls back into JS happen after this point — the Promise stays pending forever and no console.error, no rejection, no resolution ever fires.
Note presentingVC:(null) on the line above the rejection — the share controller's presentation never gets a parent assigned, confirming presentViewController: was dropped.
Steps to reproduce
- iOS device or simulator.
- A screen that uses
react-native-screens (e.g., any @react-navigation/native-stack screen).
- Issue a JS-driven navigation transition that causes the topmost iOS view controller to change.
- In the same JS task as that navigation call, invoke
Share.share(...).
- Under sufficient main-thread contention, UIKit's layout pass reattaching the now-current screen happens after the Share's
presentViewController: is processed — UIKit silently rejects, the JS Promise never settles.
Snack or a link to a repository
https://gist.github.com/BadLice/0d999371f228a314c731b0532eea624d
Screens version
4.21.0
React Native version
0.84.1
Platforms
iOS
JavaScript runtime
Hermes
Workflow
React Native (without Expo)
Architecture
Fabric (New Architecture)
Build type
Release mode
Device
iOS simulator
Device model
No response
Acknowledgements
Yes
Description
On iOS,
Share.share(...)can return a Promise that neither resolves nor rejects when the underlying UIKitpresentViewController:call is silently dropped by iOS — specifically when the presenter view controller's view is not currently in the window hierarchy. The share dialog never appears, the JS Promise stays pending forever, and the caller'stry { await Share.share(...) } catchcannot recover the call.This is a contract violation: the Promise returned by
Share.shareis documented to either resolve with{ action, activityType }or reject with an error. There is currently a third outcome — "neither" — that any caller awaiting that Promise has no way to handle.Looking at
React/CoreModules/RCTActionSheetManager.mm(showShareActionSheetWithOptions), the iOS native implementation:RCTPresentedViewController().shareController.completionWithItemsHandler(which is the only path that ever invokesfailureCallbackorsuccessCallback, i.e. the only path that settles the JS Promise).[parentViewController presentViewController:alertController animated:YES completion:nil]with nocompletion:block and no validation of the presenter.When iOS rejects the present, it does so silently: a warning is written to the system log, but the share controller is never presented, so
completionWithItemsHandleris never called. There is no other path back to JS — the Promise leaks forever.Conceptual code that triggers the bug
In practice this happens deterministically in production apps under realistic main-thread load. Inserting an iOS-only
await new Promise(r => setTimeout(r, 300))between the navigation call andShare.sharereliably masks the issue, which is independent confirmation that the cause is a main-thread timing race rather than something specific to the call site.Native log evidence
Captured via
xcrun simctl spawn booted log stream --predicate 'eventMessage CONTAINS "Attempt to present" OR eventMessage CONTAINS "UIActivityViewController"'while reproducing the bug:The last line is iOS explicitly refusing the presentation. No further calls back into JS happen after this point — the Promise stays pending forever and no
console.error, no rejection, no resolution ever fires.Note
presentingVC:(null)on the line above the rejection — the share controller's presentation never gets a parent assigned, confirmingpresentViewController:was dropped.Steps to reproduce
react-native-screens(e.g., any@react-navigation/native-stackscreen).Share.share(...).presentViewController:is processed — UIKit silently rejects, the JS Promise never settles.Snack or a link to a repository
https://gist.github.com/BadLice/0d999371f228a314c731b0532eea624d
Screens version
4.21.0
React Native version
0.84.1
Platforms
iOS
JavaScript runtime
Hermes
Workflow
React Native (without Expo)
Architecture
Fabric (New Architecture)
Build type
Release mode
Device
iOS simulator
Device model
No response
Acknowledgements
Yes