Dev.to · 3 min read

The await That Silently Breaks navigator.clipboard.writeText()

The await That Silently Breaks navigator.clipboard.writeText()

Someone on your team ships a "Copy invite link" button. It fetches a fresh, single-use link from the API, then copies it to the clipboard so the user can paste it into Slack. Code review is clean. QA clicks it a dozen times. Works every time. Two weeks later, a support ticket: "I click Copy, I paste into Slack, and I get yesterday's clipboard contents. Not the link." Another ticket, same shape, different user. You can't reproduce it. You click the button forty times in a row and it copies the link forty times. Here's the detail that breaks the case open, if you know to look for it: every user who hit this had switched to another tab or clicked into another window in the second or two between clicking Copy and the link actually landing on their clipboard. Nothing crashed. No error reached the UI. navigator.clipboard.writeText() just quietly declined to run, and the code never checked. The fixes that don't touch it The instinct is to treat it as a normal race condition or a network hiccup: Add a loading spinner so the user waits for the fetch to finish before clicking. Doesn't help — the bug isn't about clicking too early, it's about what happens after the click, while your code is still awaiting something. Wrap the write in try/catch and just eat the error. Now it fails the same amount, but silently on purpose instead of silently by accident. Worse, arguably — you've deleted your own evidence. Retry the write a moment later. If the reason it failed is still true (the document still isn't focused), the retry fails too. If you retry indefinitely, you've built a poller for a permission that has nothing to do with time. None of these ask the one useful question: why does a browser API that "just copies a string" refuse to run at all? The real rule: the API only trusts the instant of the click navigator.clipboard.writeText() is part of the Async Clipboard API, and it enforces something stricter than "the user clicked a button once, somewhere." At the exact moment you call it, the browser wants two things to still be true: The document has focus. Not "had focus when the click happened" — has it right now, this call, this tick. The user gesture is still active. Clicks grant a short-lived window of "the user just did something," and that window doesn't wait around forever. Await literally anything — a fetch() for the link, a promise chain, even a couple of re-renders — and you've inserted a gap between the click and the actual writeText() call. If the user alt-tabs, clicks a browser chrome element, or a dev-tools panel steals focus during that gap, the call arrives with the document unfocused. The browser doesn't queue it, doesn't warn the user, doesn't retry. It rejects the promise with a NotAllowedError — in Chrome, literally "Failed to execute 'writeText' on 'Clipboard': Document is not focused." — and if nothing in your code reads that rejection, it vanishes into an unhandled-promise-rejection log line nobody watches production for. // looks completely reasonable, fails silently under real-world timing async function copyInviteLink() { const res = await fetch("/api/invite-link"); //

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News