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.

navigator.canShare decision flow Flowchart showing: User gesture leads to isSecureContext check. If false, go to Fallback. If true, check navigator.canShare exists and returns true. If false, go to Fallback. If true, call navigator.share(). If AbortError, exit. If NotAllowedError or TypeError, go to Fallback. On success, done. User gesture (click) window .isSecureContext? false Fallback true canShare (payload)? false true await navigator.share() Error type? AbortError Exit NotAllowed/ TypeError

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.