Debugging navigator.share with Remote DevTools
This guide is part of Testing and Debugging Web Share Flows, which itself is part of Web Share API and Security Contexts.
The Web Share API only really exists on the devices you are least able to inspect. Your desktop browser either lacks navigator.share entirely or routes it somewhere unrepresentative, so the bug you are chasing — the sheet that will not open, the payload the target rejects, the promise that never settles — lives on a phone with no console. Remote inspection closes that gap: a full DevTools session, running on your desktop, attached to the real browser on the real handset.
Feature Detection Gate
Before debugging behaviour, confirm the environment. Most “the share button does nothing” reports resolve at this line, because the page was loaded over a LAN IP rather than a secure origin.
// share-diagnostics.js
export function shareDiagnostics(payload) {
return {
secureContext: window.isSecureContext === true,
origin: window.location.origin,
hasShare: typeof navigator.share === 'function',
hasCanShare: typeof navigator.canShare === 'function',
payloadValid: typeof navigator.canShare === 'function'
? navigator.canShare(payload)
: null
};
}
Call it from the console over the remote session — shareDiagnostics({ url: location.href }) — and the four booleans tell you immediately whether you are debugging a support problem, an origin problem, or a payload problem. If secureContext is false, stop here and fix the origin first with configuring HTTPS for local development.
Step 1 — Attach a Session to the Device
Android + Chrome. Enable Developer options on the phone (tap Build number seven times), switch on USB debugging, and connect the cable. Set the USB mode to file transfer rather than charging-only, then accept the RSA fingerprint prompt when it appears. On the desktop open chrome://inspect#devices; the phone and every open tab appear within a few seconds, and inspect opens a normal DevTools window bound to that tab. Port forwarding in the same panel lets the phone reach a dev server on your laptop through localhost, which keeps the origin a secure context — the single most useful trick in the whole workflow.
iOS + Safari. On the phone, enable Settings → Safari → Advanced → Web Inspector. On a Mac, enable the Develop menu in Safari’s Advanced settings, connect the device, then choose Develop → device name → page. You get Safari’s Web Inspector against the real page. This path requires macOS; there is no first-party equivalent on Windows or Linux.
Step 2 — Log Around the Sheet, Not Inside It
The instinctive move — a breakpoint on the line after await navigator.share(...) — does not behave the way you expect, because while the sheet is open the page is backgrounded and no JavaScript runs. Nothing pauses because nothing executes. Instrument the boundaries instead.
// instrumented-share.js
import { shareDiagnostics } from './share-diagnostics.js';
export async function debugShare(payload) {
if (!window.isSecureContext || typeof navigator.share !== 'function') {
console.warn('[share] unsupported', shareDiagnostics(payload));
return { outcome: 'fallback' };
}
console.time('[share] sheet open');
console.info('[share] invoking', shareDiagnostics(payload));
try {
await navigator.share(payload);
console.timeEnd('[share] sheet open');
console.info('[share] resolved — target accepted the payload');
return { outcome: 'shared' };
} catch (err) {
console.timeEnd('[share] sheet open');
console.warn(`[share] rejected: ${err.name} — ${err.message}`);
return { outcome: err.name === 'AbortError' ? 'dismissed' : 'failed', error: err };
}
}
The console.time pair is the most informative part. A gap of a few hundred milliseconds means the sheet never opened and the call failed immediately — usually NotAllowedError from a lost user gesture. A gap of several seconds followed by AbortError is a genuine dismissal. A gap that never closes means the promise is still pending, which on iOS normally indicates the page was reloaded underneath the sheet.
Step 3 — Reproduce the Gesture Failure Deliberately
Once the session is attached, the highest-value experiment is proving whether user activation is the culprit. Run both variants from the remote console and compare:
// gesture-probe.js — paste into the remote console, then tap the test button
export function installGestureProbe(button, payload) {
button.addEventListener('click', async () => {
try {
await navigator.share(payload); // synchronous — activation intact
console.info('[probe] direct call OK');
} catch (err) {
console.warn('[probe] direct call failed:', err.name);
}
});
button.addEventListener('dblclick', async () => {
await new Promise((r) => setTimeout(r, 0)); // one turn of the event loop
try {
await navigator.share(payload); // activation may already be spent
console.info('[probe] deferred call OK');
} catch (err) {
console.warn('[probe] deferred call failed:', err.name);
}
});
}
If the direct call opens the sheet and the deferred one throws NotAllowedError, the bug is an await sitting between the click and the share call — a fetch for a canonical URL, an image resize, a state update. Move that work before the handler and pass a ready-made payload in.
Failure Modes and Recovery
| Symptom in the remote session | Cause | Fix |
|---|---|---|
Device missing from chrome://inspect |
USB debugging off, charging-only mode, or fingerprint not accepted | Re-plug the cable and accept the prompt on the phone |
hasShare: false on a supported phone |
Page served over a LAN IP, not a secure origin | Use DevTools port forwarding so the origin stays localhost |
Breakpoint after share() never hits |
Page is backgrounded while the sheet is open | Log on settlement instead of breaking inside the pending promise |
| Sheet opens, promise never settles | Page reloaded or navigated under the sheet | Avoid re-rendering the view that owns the share handler |
NotAllowedError in under 100 ms |
User activation consumed before the call | Remove awaits between the gesture and share() |
DataError after a file payload |
Target rejected the specific files | Validate with canShare and shrink or re-type the files |
Browser and Platform Caveat
Remote inspection is a development-time tool and behaves differently across the two ecosystems. Android’s chrome://inspect gives you the full DevTools surface — network throttling, breakpoints, coverage — from any desktop OS, and its port forwarding is what makes secure-context testing painless. Safari’s Web Inspector is macOS-only and slightly more restricted: the console and debugger are complete, but the page is suspended more aggressively when the sheet is up, so timing measurements taken across the sheet boundary read longer than the user’s perceived delay. On both platforms, remember that an installed PWA and a browser tab are separate inspection targets; if your share button only misbehaves once installed, make sure you attached to the standalone window and not the tab you left open behind it.
FAQ
Why does my breakpoint never hit while the share sheet is open?
The share sheet is OS UI drawn over the browser, and the page is backgrounded while it is showing. A breakpoint set inside the pending promise cannot pause anything because no page JavaScript is running. Log on settlement instead — after the sheet closes, the promise resolves or rejects and normal execution resumes.
My phone does not appear in chrome://inspect. What is missing?
Almost always one of three things: USB debugging is off in Developer options, the phone’s USB mode is set to charging-only rather than file transfer, or the RSA fingerprint prompt was never accepted on the device. Reconnect the cable and watch the phone for the authorisation dialog.
Can I remote-debug an iPhone from Windows or Linux?
Not with Safari Web Inspector, which requires macOS. For iOS debugging from other platforms you need a device cloud or a third-party inspector bridge. Android remote debugging works from any desktop platform through chrome://inspect.