Jacob Deichert


Monkey Patching setTimeout to Override a Specific Callback

A few years ago, one of my teammates once asked how they could get our app's auth token renewal timeout to fire early for a particular automated UI test they were writing. The token renewal check was every 20 minutes or so and there's no way we would want the UI test to wait that long to verify the logic behaves correctly.

Before we continue, here's a basic example of what a token renewal timeout could look like:

const tryTokenRenewal = () => {
    const token = localStorage.getItem('USER_AUTH_TOKEN');
    authService.triggerTokenRenewal(token);
};

const delay20Mins = 1000 * 60 * 20;
setTimeout(tryTokenRenewal, delay20Mins);

Now, there is of course a variety of ways to make this timeout happen faster in a testing environment. For example, making the renewal timeout configurable for specific deployment environments could have been ideal. However, due to many other constraints at the time we weren't able to go this route just to get a single test to pass. Though, I was still intrigued by the question of whether it was possible to override a specific setTimeout... so I played around for a bit and came up with a way to do so!

First, in order to override window.setTimeout we need to be able to monkey patch it with our own implementation. If you're not familiar with monkey patching, it's just a way to replace default behaviour with custom behaviour at runtime.

Monkey patching window.setTimeout is super simple. It looks like this:

const originalSetTimeout = window.setTimeout;
window.setTimeout = (cb, delay) => {
    // Let's ignore the delay and make the callback fire immediately
    const newDelay = 0;
    return originalSetTimeout(cb, newDelay);
};

The example above makes all timeouts fire immediately! As you can see, you can inject custom code to run before or after the timeout occurs. You can even prevent the timeout altogether!

Okay, so we know how to monkey patch setTimeout to run earlier, but how do we only override it for a specific callback? Well, one naive way would be to check if the delay equals some expected value, like 20 minutes in our case.

const originalSetTimeout = window.setTimeout;
window.setTimeout = (cb, delay) => {
    const delay20Mins = 1000 * 60 * 20;
    // Hmmm... this might be our token renewal timeout
    // so let's fire it immediately.
    if (delay === delay20Mins) {
        return originalSetTimeout(cb, 0);
    }
    // Let's leave all other timeouts alone.
    return originalSetTimeout(cb, delay);
};

So this works. But, it has a little too much potential for collision with other timeouts that have the same delay. However, we can go a step further! We have access to the cb function... but what can we do with it? 🧐

After playing around for awhile, I realized you can actually get the function's raw source code by stringifying it like so:

// Stringifies the function's source
String(tryTokenRenewal); // Or tryTokenRenewal.toString()

// Which ends up looking like this:
`() => {
    const token = localStorage.getItem('USER_AUTH_TOKEN');
    authService.triggerTokenRenewal(token);
}`

This is super useful. We can search for a unique string within the source to check if it's the callback we're looking for!

Putting it all together, we get something like this:

const originalSetTimeout = window.setTimeout;
window.setTimeout = (cb, delay) => {
    const functionSource = String(cb);
    // We're checking that the source contains "USER_AUTH_TOKEN"
    // here because we know our callback has that string.
    if (functionSource.includes('USER_AUTH_TOKEN')) {
        delay = 0;
    }
    return originalSetTimeout(cb, delay);
};

That's it! As long as this monkey patch is applied before the main app code is loaded, the callback we're checking for will fire immediately after it's initialized. Injecting it before the app code is bit trickier to do depending on the use case... but that topic deserves another post.

Beware the Caveats 👻

Although checking the cb source may be more reliable than simply checking the delay, it's still far from foolproof.

If the environment you're applying this in has code minification or obfuscation enabled, that will severely limit which source strings you can depend on existing. You could grab a unique string after compile-time, but that string could change between builds and therefore may not be reliable enough for your use case. In our case above, "USER_AUTH_TOKEN" is a raw string that minifiers usually will not touch, so we can rely on it existing. But... the bigger problem is if (more like when) a teammate adds "USER_AUTH_TOKEN" to a different callback in the future or removes it from the current callback, our monkey patch will silently break.

This is why monkey patching is usually discouraged. It's an extremely powerful feature, but it can lead to bugs that are hard to track down or reason about. With that said, I've found it can be very useful for runtime debugging purposes and the odd testing scenario, but that's it. Monkey patching in production code is a whole other story... There's likely several other approaches that would work much better and be 100% reliable.

Anyway, thanks for reading and I hope you learned something here! Monkey patching is fun! 🙈🙉🙊