Implementing navigator.canShare for Graceful Fallbacks
This guide is part of Understanding Secure Context Requirements, itself a section of Web Share API & Security Contexts.
navigator.share() fails silently in production not because of missing browser support but because of unvalidated payloads, missing user-gesture bindings, and insecure execution contexts. The navigator.canShare() pre-check exists precisely to catch these failures before the share sheet is ever invoked, and to give your code a clean branching point for fallback logic.
Decision flow: canShare to fallback
The diagram below shows the full evaluation path from a user gesture to either a native share sheet or a fallback mechanism.
Feature detection gate
Always validate both the secure context and the method’s existence before touching navigator.canShare. These are two distinct checks — canShare is defined in fewer browsers than share, and neither is present on HTTP origins.
export function canNativeShare(payload) {
if (!window.isSecureContext) return false;
if (typeof navigator.canShare !== 'function') return false;
return navigator.canShare(payload);
}
Call this once at module initialisation to cache the result for text-only payloads; re-evaluate per-invocation when the payload changes (e.g. dynamically attached files).
Step-by-step implementation
Step 1 — Validate payload schema before canShare
navigator.canShare rejects payloads that violate the ShareData dictionary: missing required fields, unsupported files MIME types, or oversized file arrays. Validate eagerly so you control the error message rather than absorbing a silent false.
const MAX_SHARE_BYTES = 10 * 1024 * 1024; // conservative 10 MB for iOS
export function validateSharePayload(data) {
if (!data.url && !data.text && !data.title && !data.files?.length) {
throw new TypeError('ShareData must include at least one of url, text, title, or files.');
}
if (data.files?.length) {
const totalSize = data.files.reduce((sum, f) => sum + f.size, 0);
if (totalSize > MAX_SHARE_BYTES) {
throw new RangeError(`Payload ${totalSize} bytes exceeds the safe sharing limit.`);
}
}
return data;
}
Step 2 — Bind share invocation directly to the user gesture
The Web Share API requires a “user activation” — a gesture that is synchronous and current at the time navigator.share() is called. Introducing await, setTimeout, or a fetch callback between the event and the API call consumes the activation token and triggers NotAllowedError.
// Correct: share() is called synchronously inside the click handler
button.addEventListener('click', async () => {
const payload = validateSharePayload({ title: doc.title, url: location.href });
await handleShare(payload);
});
// Wrong: activation is lost by the time share() is reached
button.addEventListener('click', () => {
fetch('/api/share-meta')
.then(r => r.json())
.then(payload => navigator.share(payload)); // NotAllowedError here
});
Step 3 — Route through canShare, then share, with typed error handling
export async function handleShare(payload) {
if (!canNativeShare(payload)) {
return triggerFallback(payload);
}
try {
await navigator.share(payload);
} catch (err) {
if (err.name === 'AbortError') return; // user dismissed — not an error
console.error(`Share failed [${err.name}]:`, err.message);
return triggerFallback(payload);
}
}
AbortError is the user closing the share sheet without selecting a target. It is not a failure — do not route it to a fallback or log it as an error. Every other rejection should trigger your fallback path.
Step 4 — Implement the fallback hierarchy
When native sharing is unavailable, route through a ranked fallback chain rather than a single option. The Clipboard API covers all modern browsers and is the highest-fidelity silent fallback for text and URLs. For file payloads, a custom modal with a manual download link is the reliable floor.
export async function triggerFallback(payload) {
const text = payload.url ?? payload.text ?? payload.title ?? '';
// Attempt 1: Clipboard API (requires its own secure-context check)
if (navigator.clipboard?.writeText && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
showToast('Link copied to clipboard');
return;
} catch {
// clipboard permission denied; fall through
}
}
// Attempt 2: Custom share modal
renderShareModal(payload);
}
For PWA contexts where users may be offline, consider queuing the share intent with syncing offline share queues via Service Workers so it resolves when connectivity returns.
Failure modes and recovery
| Symptom | Root cause | Minimal fix |
|---|---|---|
TypeError: navigator.canShare is not a function |
Firefox, older Chrome, or HTTP origin | Guard with typeof navigator.canShare === 'function' after window.isSecureContext |
NotAllowedError on a valid payload |
User gesture consumed by async middleware | Move navigator.share() directly inside the synchronous event handler |
| iOS Safari silently drops the dialog | Payload missing url/title, or unsupported MIME in files |
Pre-validate with validateSharePayload; always include url and title |
| Memory leak and eventual tab crash | URL.createObjectURL() references never revoked |
Call URL.revokeObjectURL() in the finally block after each share attempt |
Desktop PWA: canShare returns true but share sheet never appears |
Some desktop OSes have no registered share targets | Always wrap in try/catch; route NotAllowedError to clipboard fallback |
Browser and platform caveats
navigator.canShare with a files array is supported on Chrome 89+ (Android), Safari 15.1+ (iOS/macOS), and Edge 89+. Firefox does not implement either canShare or share as of mid-2026 — desktop Firefox users will always reach your fallback path. On macOS, even when canShare returns true, the available share targets depend entirely on which apps are registered with the OS sharing extension mechanism, so a NotAllowedError catch remains necessary on all platforms. Consult the browser support matrix for version-level detail.
FAQ
Why does navigator.canShare return true but navigator.share still throws?
canShare validates payload schema and confirms the secure context is present. It does not verify that a live user activation exists, that the OS has registered share targets, or that system-level restrictions permit sharing. Always wrap navigator.share() in try/catch.
How do I handle Web Share API in PWAs installed on desktop?
Desktop PWAs may have no native share targets even when canShare returns true. Detect via canShare, call share, then catch the NotAllowedError and route to navigator.clipboard.writeText or a custom modal.
What is the maximum payload size for the Web Share API?
iOS typically caps individual files near 10 MB; Android/Chromium allows up to roughly 50 MB. Validate file.size before calling canShare to avoid silent failures on low-memory devices.
Can I call navigator.canShare from a Web Worker?
No. The Web Share API is main-thread only and requires a direct user interaction. Calling it from a Worker throws TypeError. Offload heavy payload construction (e.g. image resizing) to Workers, then post the serialised data back to the main thread before invoking share.
Related
- Understanding Secure Context Requirements — parent guide covering
window.isSecureContext, HTTPS enforcement, and the full secure-context gate - How to Fix navigator.share is undefined in Chrome — sibling guide diagnosing the common
undefinedsymptom - Browser Support Matrix for the Web Share API — version-level support table across Chrome, Safari, Firefox, and Edge
- Mastering the Clipboard API for Rich Text — deep dive into the clipboard fallback layer used when
canSharereturns false - SMS and Email Fallback Architectures — broader fallback strategy when both native share and clipboard are unavailable