The async Clipboard API can be denied where execCommand('copy') still works
Fact
navigator.clipboard.writeText is gated by a permission that the embedding browser decides. Embedded views (desktop-app webviews, in-app browsers) can reject it with NotAllowedError: Write permission denied inside a real click, in a secure context, with clipboard-write allowed by permissions policy.
document.execCommand('copy') on a selected <textarea> is gated only by user activation, so it still succeeds in that same click — even after awaiting the rejected writeText, because transient activation lasts a few seconds (verified in Chromium).
Why it matters
A "Copy" button that only uses the async API works in a normal browser tab and fails for everyone opening the page inside another app — exactly where links to shared results tend to be opened.
Neither path works from a script-dispatched element.click(), which carries no activation, so automation reports "copy failed" for both and hides which one real users would hit. Test with real input.
How to apply
- Try
writeTextfirst; on rejection, put the text in an off-screen read-only<textarea>,select()it, callexecCommand('copy'), remove it, and return focus to the button. - Report failure only when both paths fail.
- Do the fallback in the same click handler; do not defer it behind a timer or a second user prompt.