Build1 distinct publisher3 min readPublished
The Async Clipboard API rechecks document focus and user activation at the moment the call executes, so a fetch placed before the write turns a Copy button into a bug that only fires when the user looks away.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
Two conditions are checked at the moment the call executes, and neither is latched when the user clicks: the document has focus at that tick, and the user gesture is still active [4]. That is the whole mechanism. Putting `await fetch(...)` in front of the write moves the call site from "inside the click" to "whenever the network came back" [5]. The write is not queued behind the focus check and there is no warning or retry; instead the browser rejects the returned promise with `NotAllowedError` [6]. In Chrome the rejection carries `Failed to execute 'writeText' on 'Clipboard': Document is not focused.` [7]. If nothing in the code reads that rejection, it becomes an unhandled-rejection line in a log nobody watches for production [8].
The mechanism also eats the three fixes people reach for first. A spinner makes the user wait before clicking, but the failure happens after the click, while the code is still awaiting [9]. A `try/catch` around the write leaves the failure rate identical and deletes the only evidence [10]. A retry runs the same check against the same unfocused document, and a retry loop is a poller for a permission that has nothing to do with elapsed time [11].
This is a sequencing problem. There are two orderings that hold: either you already have the string when the handler runs, so `writeText()` executes in the click's own tick, or you hand the clipboard a promise. `writeText()` only accepts a string you have in hand, but `navigator.clipboard.write()` takes a `ClipboardItem` whose data may be a promise that resolves later [12]. Call `write()` synchronously in the handler and the permission check passes at the one moment it is guaranteed to be true; the browser holds the slot open and fills it when the promise settles [13]. Either way, read the returned promise and put the failure somewhere a human sees it, because the silent path is what made this unreproducible in the first place [8].
The unreproducibility has a direct cause. The failure needs focus to be lost inside the await window, so a tester clicking with the tab focused cannot produce it, no matter how many times [15]. Forty consecutive successful clicks is the expected result of testing it wrong [2]. The bug only fires when the tab has lost focus while the write is still pending, which is exactly the moment nobody is watching.
Which ordering I would pick depends on what the fetch mints. Prefetching is the cheaper option in most cases, since it needs no capability check and moves the failure into the fetch, which you already monitor. But the button in this scenario fetches a single-use link [1], and prefetching on mount or hover means minting links that nobody pastes. For that shape, the promise-valued `ClipboardItem` is the correct tool rather than the clever one. The article does not publish an engine support matrix for it [16], and that is the one thing I would confirm before shipping it.
Ranked by verification strength, evidence, and original report placement.
In the dev.to scenario, a 'Copy invite link' button fetches a fresh, single-use link from the API and then copies it to the clipboard; code review was clean and QA clicked it a dozen times with it working every time.
Support tickets reported that clicking Copy and pasting into Slack produced yesterday's clipboard contents instead of the link, and the team could not reproduce it, clicking the button forty times in a row and getting the link every time.
Every user who hit the bug had switched to another tab or clicked into another window in the second or two between clicking Copy and the link landing on their clipboard.
navigator.clipboard.writeText() is part of the Async Clipboard API, and at the exact moment the call is made the browser requires that the document has focus right then (not that it had focus when the click happened) and that the short-lived user gesture window is still active.
Awaiting anything, including a fetch() for the link, a promise chain, or a couple of re-renders, inserts a gap between the click and the actual writeText() call, so the write happens whenever the network resolves rather than while the click is still fresh.
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 does not queue it, does not warn the user, does not retry, and rejects the promise with NotAllowedError.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · September 3, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
In Manifest V3 every message handler is a cold start, and the fix for lost state hides new failures1 distinct publisher
build
The exit ping that never left: fetch in beforeunload loses to the browser's unload policy1 distinct publisher
build
The stroke width that never rendered: SVG attributes lose every cascade fight1 distinct publisher
build
Three ways to ask who embedded your iframe, and only one the host cannot switch off1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Checkable mechanics, undocumented rule, unverifiable anecdote
The technical core is unusually easy to falsify for a single-source post — Chrome's "Document is not focused" rejection is quoted word for word, and the two code samples either behave as described or they don't. What is asserted rather than shown is the rule itself: the focus and user-activation requirement arrives with no link to the specification or vendor docs, and the piece's one outside citation is MDN, used for execCommand's deprecation instead. The support-ticket narrative that opens everything has no ticket, no log line, and no repro steps attached.
Nothing counted
Nobody measures anything in this reporting. There is no figure for how often the write is refused in the wild, no share of users who switch tabs mid-fetch, no telemetry from any deployment, and no indication of how widely the promise-valued ClipboardItem pattern is actually used. The forty clicks and the two tickets are storytelling devices, not counts.
The fix is sold a step ahead of its support matrix
Restraint is the norm through most of this: no drama about a browser bug, no claim that the API is broken, just a sequencing mistake and three fixes that don't work. The overreach is confined to one move — write() with a promise-backed ClipboardItem is presented as "the actual tool" for this situation while the caveats section discusses secure contexts and execCommand instead of the question a reader will hit first, which is whether their target browsers accept a promise there at all.
Explainer with a funnel bolted on
The post ends by asking readers to take an eight-question quiz and sign up free at the author's own site, which tells you what shape the argument had to take: one memorable rule, a clean before-and-after, a rhetorical question inviting confession. That pressure runs against caveats and support tables, and it shows in what got trimmed — the browser-compatibility footnote the fix needs, and any pointer to the spec that would let a reader check the rule without the author.
Solid on the why, soft on the where
We can stand behind the causal story — a call that re-checks focus at execution time, an await that moves the call past a tab switch, a rejection that nothing reads — because it is internally consistent, matches the quoted Chrome message, and is cheap for any reader to test. Confidence drops on two things this reporting cannot settle alone: whether the recommended pattern works across the browsers a team ships to, and whether the framing incident happened as told.