I ran into a really confusing bug a while back. It was related to a small utility function that I had written:
export async function pause(seconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
This is a pretty simple function that returns a promise that resolves after the given number of seconds.
Here's an example usage:
async function killProcess(reason: string) {
logger.info(`Killing process: ${reason}`);
await pause(1); // Give logger 1 second to flush to external system
process.exit(0);
}
The Issue
The code above looks fairly harmless. It doesn't contain any bugs, at least not in the strict sense. But it does contain a gotcha.
Let's look at the method signatures for the built-in JavaScript functions that take a time parameter:
function setTimeout(callback, ms);
function setInterval(callback, ms);
Notice anything about these compared to the pause
function above? setTimeout
and setInterval
both take time as milliseconds, but pause
takes time as seconds.
Why is this is a big deal? Because pause
violates the principle of least surprise. When we write JavaScript, we should expect to pass time in as milliseconds, because that's what the standard library accepts and therefore, that's what we're used to doing.
In fact, the reason I discovered this issue was because I had the following code:
async function killProcess(reason: string) {
logger.info(`Killing process: ${reason}`);
await pause(1000); // Give logger 1 second to flush to external system
process.exit(0);
}
I could not for the life of me figure out why killProcess
was not actually killing the process. It turned out I was telling it to pause for 1,000 seconds!
The Fix
My initial instinct was to fix the call from pause(1000)
to pause(1)
. But then I realized, there's a reason I initially passed in 1000
, and that reason was a good one - I was used to passing in milliseconds!
So rather than fixing the usage of the function, I fixed the function itself to accept milliseconds instead of seconds.
Other Examples
Another example that always drives me crazy is how jQuery handles the parameters passed into .each
:
$('li').each((i, el) => { ... });
Why does this drive me crazy? Because the native .forEach
method in JavaScript looks like this:
[1, 2, 3].forEach((el, i) => { ... });
Notice how el
and i
are opposite between the two methods. So which is correct? Well, there is no objective correct order, but the standard library's usage should always be the winner because it is, by definition, the standard.
The key takeaway is to always think about how your parameters compare to the standard library or more generally, the standards of the language.