A “copy embed code” button quietly stopped working, and nothing on our side had changed. There was no deploy, no commit anywhere near the copy button, and no error report from anyone, because it failed silently. For a slice of users on a slice of browsers, clicking “copy” just did nothing, while it kept working fine for everyone else, which is the worst flavor of bug because it makes you doubt your own ability to reproduce it.

The cause had nothing to do with our code. A decade-old JavaScript library and a brand-new browser API were fighting over the same global variable name, window.Clipboard. The library claimed it in 2016. The browser eventually claimed it back. Whichever one ended up bound to the name won, and when the browser won, our new Clipboard(...) call threw.

The setup

The app is Classic-ASP on IIS and MSSQL. No module bundler, no webpack, no import maps. Front-end dependencies are minified files dropped in a lib/ vendor folder and pulled in with a plain <script> tag, where each library hangs itself off a global. For copy-to-clipboard I’d been using clipboard.js v1.5.9, vintage 2016, vendored in that folder.

clipboard.js ships as a UMD bundle, and its wrapper does what every UMD library from that era did. In the browser-global branch:

e.Clipboard = t()

where e is window. So loading that file plants the constructor on window.Clipboard. Our tournament embed page did exactly what the 2016 docs told you to do:

var clipboard = new Clipboard("#copy_button");

That line worked for years, because in 2016 window.Clipboard was unclaimed territory and the library was free to squat on it. Then browsers shipped the Async Clipboard API.

The collision

Modern browsers now expose a native Clipboard object on window, the one behind navigator.clipboard. It’s a real platform object, and it is not constructible. You cannot new it. There is no constructor for you to call.

So now two things answer to the identifier Clipboard in global scope: our 2016 library’s constructor, and the browser’s native, non-constructible object. Which one wins comes down to load order and browser. When the native object ended up bound to window.Clipboard, our new Clipboard("#copy_button") hit the native object and threw:

TypeError: Clipboard is not a constructor

Some engines phrase it as an illegal invocation instead. Either way, the exception fired during page setup, the copy handler never got wired up, and the button silently did nothing. No server error, no log line, no symptom at all unless you happened to open devtools on an affected browser.

Our code was byte-for-byte the same as the day it shipped. The library file hadn’t changed either. The thing that changed was the browser, out from under both of us, claiming a name a third-party lib had assumed was its own forever.

The fix, without bumping the dependency

The obvious move is to upgrade clipboard.js. Newer builds dodge this exact collision by exposing themselves as window.ClipboardJS instead of window.Clipboard. Problem solved, in theory.

I did not do that as the first move. Here was the calculus. That minified library is loaded across many pages of a revenue app with no bundler and no test harness around its integration points. A version bump on a 2016 lib crosses years of API churn, and its blast radius is something I can’t cheaply bound in a Classic-ASP app where I can’t just run a suite over the change. The break itself was a single call site. The lower-risk fix is a defensive shim around that one call, not a dependency jump that could ripple into pages I’m not looking at today. The upgrade can happen later, deliberately, on its own ticket, not under fire.

So the fix feature-detects the constructor and degrades through three levels.

The first is figuring out whether we actually have a usable constructor, regardless of which name it’s hiding under.

var ClipboardCtor = (window.ClipboardJS || window.Clipboard);
if (typeof ClipboardCtor === 'function') {
try {
new ClipboardCtor("#copy_button");
} catch (e) {
console.error('clipboardjs init error:', e);
}
}

The typeof ClipboardCtor === 'function' check and the try/catch work as a pair. Newer clipboard.js exposes ClipboardJS; older builds expose Clipboard. A global can satisfy a shallow type check without being usable as this library constructor, so the catch prevents an initialization failure from killing page setup. Preferring ClipboardJS || Clipboard also means an eventual library upgrade keeps working. We do not assume the constructor exists. We prove it at the call site.

When there’s no library constructor at all, the next level is the native API directly.

navigator.clipboard.writeText(text);

The same browser change that broke us is also the thing that lets us copy without a library in the first place. If the platform claimed the name, we let it do the work.

The floor for old browsers with neither is the execCommand trick. Build a throwaway <textarea>, push it offscreen, select it, copy, clean up.

var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-999999px';
ta.style.top = '-999999px';
document.body.appendChild(ta);
ta.select();
ta.setSelectionRange(0, 99999); // mobile
try {
document.execCommand('copy');
} finally {
document.body.removeChild(ta);
}

The setSelectionRange(0, 99999) is there because some mobile browsers ignore select() on a programmatically created textarea and need an explicit range. The finally removes the node whether or not the copy threw, so we don’t leak an offscreen textarea on every failed click.

One more detail worth stealing: the embed code itself isn’t trusted to a single source. It’s stashed on the button as a data-clipboard-text attribute, and there’s a backup read from a fallback element’s .textContent. If one path is empty, the other fills in. The copy mechanism and the copy payload both have a fallback.

That’s the whole fix. No dependency bump, no build step, one defensive call site. It shipped in a same-day hotfix, alongside a separate bracket-page redirect fix that went out earlier that morning. Two production hotfixes before lunch, which is its own kind of normal here.

A global named after a generic noun is a time bomb. Clipboard, Storage, URL, Notification, PaymentRequest. If a useful third-party library plants one of those on window, you’re betting the platform will never want that name. The platform can reclaim a generic name in any future browser release, and it never asks first. clipboard.js v1.5.9 made that bet in 2016 and lost it a decade later, with the bill landing on a copy button nobody had touched.