Keep Your Parameters Consistent with the Standard Library

Aug 26, 2024

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.