The bug class where your monkey-patch silently does nothing
TLDR: Monkey-patching window.fn = wrap(window.fn) works only for callers that dereference window.fn each time they call it. Event handlers and stored references captured the original value already, so they bypass your patch entirely. The patch installs cleanly, throws no error, and never runs.
The bug class
You want to add behavior to an existing function you do not own. The classic move is to wrap it:
const orig = window.doThing;
window.doThing = function () {
orig();
myExtraStuff();
};
This is correct only if every caller reaches the function through window.doThing at the moment they call it. If any caller grabbed the reference earlier and held onto it, that caller still points at the original. Your wrapper is installed on the global name, but the caller never reads the global name again.
The tell: your added behavior does not happen, there is no error, and the original behavior works perfectly. That combination is the signature of a patch that is being bypassed.
Why the language produces it
In JavaScript, a function is a value. When you write:
button.addEventListener('click', doThing);
you pass the current value of doThing to the event system. The listener now holds that value directly. It has no relationship to the variable or the global property you read it from. Later reassigning window.doThing changes what the name points to, but the listener already copied the old pointer.
This is not a quirk, it is just how values work. The mistake is assuming that patching a name reaches everyone who ever used that name. It reaches only those who use it fresh, each call.
The concrete instance
We had a data pipeline with a run function bound to a button:
$('#runBtn').on('click', runPipeline); // captured the value here
Separately, an injected module tried to add "emit results as tagged data" after each run, by wrapping the global:
const orig = window.runPipeline;
window.runPipeline = function () { orig(); emitTags(); };
The button kept calling the original runPipeline. emitTags never fired. The pipeline ran fine, the results rendered fine, and the tags we needed downstream simply never appeared. Nothing logged, nothing threw. It looked like the emit code was never reached, because it never was.
The fix
Do not patch the name. Rebind the actual caller. The button is the thing that captured the old value, so replace what the button calls:
$('#runBtn').off('click', origHandler).on('click', function () {
runPipeline();
emitTags();
});
Now the caller invokes the wrapper directly. There is no reliance on anyone dereferencing a global at call time, because we changed the reference the caller actually holds.
If there are many captured callers, patch at the source instead: wrap the value before anything captures it, or expose an explicit hook the original function calls (onComplete) so extension does not require interception at all.
How to prevent the class
Before monkey-patching a global function, ask: who calls this, and do they read the global each time or did they capture it? Event listeners, setTimeout callbacks, stored callbacks, and anything passed as an argument have captured the value. Your patch will not reach them.
When you can, design for extension instead of interception. A function that fires a completion callback or emits an event is patchable by everyone, forever, without this trap. Interception is a last resort for code you cannot change, and even then, rebind the caller, do not just reassign the name.
The one line to keep
Reassigning window.fn retargets the name, not the references people already hold. A handler bound with the old value keeps running the old value. If your monkey-patch does nothing and nothing errors, you are almost certainly patching a name whose callers captured the function long ago.