The Bug Class Where Your Monkey-Patch Silently Does Nothing
TLDR
Reassigning global functions (window.fn = wrappedFn) fails to intercept callers that cached the original function reference during initialization. Event listeners bound prior to reassignment continue calling the un-wrapped reference. Explicitly rebinding element listeners or utilizing completion callbacks ensures wrapper execution.
| Interception Strategy | Lookup Behavior | Event Listener Invocation | Execution Reliability |
|---|---|---|---|
| Global Name Reassignment | Dereferenced at call time | Executes stale cached reference | Fails silently (Bypassed) |
| Caller Rebinding / Callbacks | Directly bound handler reference | Executes updated wrapper | 100% Guaranteed Execution |
Early-bound event handlers cache function pointers, causing reassigned global wrappers to fail silently
Wrapping global functions to add post-processing behavior is a common JavaScript pattern:
// Vulnerable to stale reference caching
const originalFn = window.runPipeline;
window.runPipeline = function() {
originalFn();
emitResults();
};
However, if an event listener cached window.runPipeline during initialization ($('#btn').on('click', runPipeline)), clicking the button invokes the stored reference directly, bypassing global reassignments silently.
Dynamically dereferencing globals inside anonymous closures ensures wrapper execution
To ensure post-processing routines execute reliably, we updated event handlers to call global functions dynamically at invocation time:
STALE CACHED POINTER (INTERCEPTION BYPASSED):
[Init Time] ────> el.addEventListener('click', runPipeline) (Caches Pointer 0x111)
[Patch Time] ───> window.runPipeline = wrappedPipeline (Updates pointer to 0x222)
[Click Time] ───> Executes Pointer 0x111 (Calls original runPipeline directly, bypassing wrapper!)
DYNAMIC LOOKUP CLOSURE (INTERCEPTION SUCCEEDS): [Init Time] ────> el.addEventListener('click', () => { window.runPipeline() }) [Patch Time] ───> window.runPipeline = wrappedPipeline (Updates pointer to 0x222) [Click Time] ───> Calls closure ──> Resolves window.runPipeline ──> Executes 0x222 (Wrapper runs!)
Here is the implementation to resolve globals dynamically:
// 1. Dynamic dereferencing wrapper prevents stale reference caching
$('#runBtn').off('click').on('click', () => {
window.runPipeline(); // Evaluates current window pointer at click time
emitResults();
});
// 2. Alternatively, expose explicit lifecycle hooks export function registerPipelineHook(callbackFn) { pipelineHooks.push(callbackFn); }
Dereferencing window.runPipeline() inside the event handler ensures reassignments execute as expected.
Rule of thumb: Resolve global methods dynamically inside closure callbacks rather than passing raw function variables to early event bindings.