Copy Fail: I Reproduced the Most Viral HN Bug in My Own Code and Found Something Worse
I was wiring up a "Copy token" button in an admin panel when the Clipboard API threw me an undefined without a single error in the console. The user would have clicked the button, seen the green check, and pasted nothing into their terminal. Or worse: pasted whatever was already in their clipboard — which in that context could be literally anything.
That's when I remembered the Copy Fail post that was sitting at #1 on Hacker News with 977 points. I went and read it. It was a solid analysis. But it was missing the part that mattered most to me.
The Copy Fail Viral Bug: What HN Says and What It Leaves Out
The original post documents a real and genuinely annoying behavior: navigator.clipboard.writeText() fails silently in certain contexts. No exception. No visible promise rejection if you don't handle it properly. Nothing. The user clicks, the icon flips to a checkmark, and the clipboard sits there untouched.
The HN thread exploded because it's behavior everyone has seen at some point and nobody knows exactly why it happens. The answers range from "it's Chromium's permission model" to "it's the iframes" to "the document doesn't have focus." All of them are correct. None of them tell the whole story.
My take: the problem isn't that the clipboard fails. The problem is that we build UX that assumes the clipboard never fails — and that assumption is most dangerous when the content being copied is a password, an API token, or a private key.
I reproduced the bug in my own environment. Here's what I found.
Reproducing the Copy Fail in Next.js: The Environment Matters More Than You Think
I opened a component I already had running in production — a button to copy API keys in an admin panel. Stack: Next.js 15, TypeScript, running on Railway behind a reverse proxy.
First surprise: the bug doesn't reproduce the same way in every context. I needed three different scenarios to understand what was actually going on.
Scenario 1: iframe Without Explicit Permissions
// ❌ Fails silently if the component lives inside an iframe
// without the allow="clipboard-write" attribute
async function copyToken(token: string): Promise<void> {
// This promise can resolve without doing anything if the document
// doesn't have the clipboard permission active in the current context
await navigator.clipboard.writeText(token);
setCopied(true); // ← runs anyway. The user sees the green check.
}
I added explicit logging to actually see it:
// ✅ Version that at least doesn't lie
async function copyTokenSafe(token: string): Promise<boolean> {
try {
// Check permission BEFORE attempting to write
const permission = await navigator.permissions.query({
name: "clipboard-write" as PermissionName,
});
if (permission.state === "denied") {
console.warn("[clipboard] Permission denied — falling back to execCommand");
return copyWithFallback(token);
}
await navigator.clipboard.writeText(token);
return true;
} catch (error) {
// Here's the problem: in some contexts the error never reaches here
// The promise resolves with undefined and doesn't throw
console.error("[clipboard] Caught error:", error);
return copyWithFallback(token);
}
}
function copyWithFallback(text: string): boolean {
// The old document.execCommand trick — deprecated but works
// where the Clipboard API has no access
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand("copy");
document.body.removeChild(textarea);
if (!success) {
console.error("[clipboard] execCommand also failed — no clipboard available");
}
return success;
}
Scenario 2: The Document Lost Focus
I found this in a specific flow: the user clicks a button that opens a modal, the modal has an autoFocus on an input, and the clipboard write fires before the document reclaims focus from the main context. Result: it fails. No warning.
// In a modal with autoFocus, this can fail if it runs
// in the same tick as the focus change
const handleCopyInModal = async () => {
// ❌ Race condition with the modal's focus change
await navigator.clipboard.writeText(apiKey);
};
// ✅ Force the write to happen AFTER the document
// has stable focus
const handleCopyInModal = async () => {
await new Promise((resolve) => requestAnimationFrame(resolve));
await navigator.clipboard.writeText(apiKey);
};
Scenario 3: HTTPS Required and the Railway Case
navigator.clipboard flat out doesn't exist in non-HTTPS contexts, except on localhost. In production behind Railway I had no issues, but when I tested in a staging environment with a custom domain whose certificate hadn't fully propagated yet — total silence. navigator.clipboard was undefined. The code didn't blow up because the catch never fired — the object simply didn't exist.
// Basic guard that should be in EVERY project using clipboard
function clipboardAvailable(): boolean {
// Checks for object existence AND secure context
return (
typeof navigator !== "undefined" &&
!!navigator.clipboard &&
window.isSecureContext
);
}
async function copy(text: string): Promise<{ success: boolean; method: string }> {
if (!clipboardAvailable()) {
// Instead of failing silently, we log the attempt
console.warn("[clipboard] Insecure context or API unavailable");
return { success: false, method: "none" };
}
try {
await navigator.clipboard.writeText(text);
return { success: true, method: "clipboard-api" };
} catch {
const fallbackSuccess = copyWithFallback(text);
return {
success: fallbackSuccess,
method: fallbackSuccess ? "execCommand" : "none",
};
}
}
What the Viral Post Doesn't Say: The Silent Security Problem
This is the part that concerns me most, and I didn't see it mentioned anywhere in the HN comments.
When the clipboard fails in a generic UI — copying a URL, a hashtag, an article title — the worst case is a frustrated user. Fine. Now think about the contexts where we actually use "Copy" most in development:
- API tokens in admin panels
- Generated passwords in web-based password managers
- Private keys in wallet or crypto service onboarding flows
- Environment secrets in Railway, Vercel, Supabase dashboards
In those cases, the user's typical flow is: generate → copy → close or navigate away → paste somewhere else. If the clipboard fails silently between steps 2 and 3, the user never sees that value again. The token lives on the server. The secret is already masked. The window is gone.
What the user did: pasted whatever was already in their clipboard, which could be:
- A code snippet from a previous session
- A password from a different account
- A chat message
- Or literally nothing
And in some onboarding flows, that error isn't caught until the service is already configured with the wrong credentials.
I measured this in my own panel: out of 47 interactions with "Copy" buttons I logged over one week, 3 triggered the fallback to execCommand. Of those 3, 2 would have been completely silent without the guard. The contexts: Safari on iOS 16 and Chrome inside an embedded documentation iframe.
Not a huge number. But if those 2 events had been API key copies, those users would have continued their flow absolutely convinced they had the token in their clipboard.
This connects to something I'd already been thinking about since I analyzed AI usage logs after the OpenAI-Microsoft break: the most expensive problems aren't the ones that throw a 500 error. They're the ones that complete successfully but with the wrong output.
The Most Common Clipboard Mistakes in Production
1. Showing visual feedback without confirming actual success
The most frequent mistake. The check icon activates in the .then() of the promise — but that promise can resolve without having copied anything. The fix: validate the helper's return value and show differentiated states.
2. No fallback to execCommand
Deprecated, yes, but with support in contexts where the Clipboard API can't reach. Not having it means users in legacy contexts or with restrictive permissions have no way out.
3. Assuming HTTPS guarantees clipboard access
HTTPS is a necessary condition, not a sufficient one. The iframe needs allow="clipboard-write". The document needs focus. User permissions can be denied at the browser level or the OS level.
4. Not logging clipboard failures
If you don't have a log of when and where the clipboard fails, you're making UX decisions blind. Three lines of logging can tell you what percentage of your users hit the failure.
5. The "Copied!" toast that never should have existed as-is
A generic success toast is fine for a URL. For credentials, the component should indicate what was copied, when, and — if the failure occurs — offer an explicit alternative: show the value again or allow manual selection.
This kind of UX debt is what bothers me most because the same thing happens with code ownership when agents generate it: nobody takes responsibility for the result until it's already too late.
FAQ: Clipboard API, Permissions, and the Copy Fail
Why doesn't navigator.clipboard.writeText() throw when it fails?
In some contexts, the promise resolves with undefined instead of rejecting. This happens especially when the document doesn't have active focus at the time of the call, or when the permission wasn't explicitly denied but also isn't guaranteed. The behavior isn't consistent across browsers — Chromium tends to resolve silently, Firefox in some cases actually rejects.
Is document.execCommand('copy') still viable in 2025?
Yes, as a fallback. It's been marked deprecated for years but still works in all major browsers. The difference: execCommand requires a selectable element in the DOM, while the Clipboard API works directly with strings. For production, use the Clipboard API with fallback to execCommand — not the other way around.
How do I check in real time whether the clipboard is available?
With navigator.permissions.query({ name: 'clipboard-write' }). Returns granted, denied, or prompt. But watch out: in Firefox, that query can throw a TypeError because not every browser implements the same list of queryable permissions. You need a try-catch around the query itself.
Is the document focus problem reproducible across all browsers?
Mostly in Chromium. Chrome and Edge require document.hasFocus() to return true for the Clipboard API to work without additional permissions. Firefox is more permissive on this front. Safari has its own logic: it allows the write only if it happens inside a user interaction event handler (click, keydown) — not inside promises or timeouts.
How does this affect components that copy inside embedded iframes?
The iframe needs the allow="clipboard-write" attribute on the HTML element. If the iframe is embedded by a third party (your documentation inside someone else's app), that third party controls the attribute — you can't force it from inside. In those cases, the fallback to execCommand is the only realistic option.
Is there a library that handles all these cases automatically?
copy-to-clipboard on npm covers the execCommand fallback. use-clipboard-copy for React handles state and retries. But none of them will give you the logging layer or the differentiated feedback for credential scenarios — that logic you have to build yourself based on your business context. Same lesson I got when I explored LocalSend as an AirDrop replacement: the tradeoffs libraries hide are exactly the ones that matter most in environments with permission restrictions.
The Conclusion the HN Thread Skipped
The Copy Fail post is good. The thread is entertaining. But 977 points of discussion and the general consensus landed on "the Clipboard API is weird" — which is true, but that's the easy part.
The hard part is accepting that we designed entire onboarding screens, token generators, secret configurators, assuming navigator.clipboard.writeText() always works. That assumption has a concrete cost: users who believe they copied something they didn't, and who are going to find out at the worst possible moment.
My position: any "Copy" button that exposes credentials needs, at minimum, three things that most don't have. First, a guard that checks isSecureContext and the object's existence before attempting anything. Second, a real fallback to execCommand with success detection. Third, a differentiated UI state for failure — not the same generic toast you use for copying a URL.
It's not about the Clipboard API being weird. It's about sensitive systems needing defensive design at every layer, including the ones that look trivial.
Same thing I learned when I simulated the Mercor attack against my own AI data stack: the vectors that look minor are the ones nobody audits. The silent clipboard is the same problem wearing different clothes.
If you're using TypeScript, the type system won't save you here either — as we saw in the TypeScript 7 benchmark, the type system solves certain structural problems but not runtime ones in browser APIs. The Promise<void> from writeText is perfectly typed and perfectly dishonest at the same time.
Go check the copy buttons in your admin panels. Not for the HN bug. For your own.
This article was originally published on juanchi.dev
Top comments (0)