Clicking “copy embed code” did nothing, 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 the click was dead, while it kept working for everyone else. That 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. An older JavaScript library and a brand-new browser API were fighting over the same global 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 an older web application on IIS and MSSQL. There is 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, and each library hangs itself off a global. For copy-to-clipboard we had 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. Loading that file plants the constructor on window.Clipboard. Our event embed page did exactly what the 2016 docs said to do:

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

That line worked for years, because in 2016 window.Clipboard was unclaimed 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 is a real platform object, and it is not constructible. You cannot new it. There is no constructor to call.

So two things now answer to the identifier Clipboard in global scope: our 2016 library’s constructor, and the browser’s native object. Which one wins comes down to load order and browser. When the native object was the one bound to window.Clipboard, our new Clipboard("#copy_button") hit it 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. There was no server error and no log line. The only symptom was in devtools, if you happened to open them on an affected browser.

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

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.

We did not do that as the first move. That minified library loads across many pages of a revenue app with no bundler and no test harness around its integration points. A version bump on a 2016 library crosses years of API churn, and in an older web application where we can’t just run a suite over the change, we can’t cheaply bound what it would touch. The break itself was a single call site, so the lower-risk fix was a defensive shim around that one call. The upgrade can happen later, deliberately, on its own ticket.

The shim feature-detects the constructor and degrades through three levels.

The first is working out whether we have a usable constructor, whichever name it hides 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, and older builds expose Clipboard. A global can pass a shallow type check and still be unusable as this library’s constructor, so the catch keeps 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 is no library constructor at all, the next level is the native API directly:

navigator.clipboard.writeText(text);

The browser change that broke the button also makes copying possible 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 a failed click doesn’t leave an offscreen textarea behind.

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

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

What a generic global name costs

A global named after a plain noun is a time bomb. Every older third-party library on your pages is borrowing a name the platform may claim later, and a browser update can break a feature with no deploy and no error report. Check which of those libraries load on your pages, and whether each call site has a fallback.