JavaScript: Timer Params

oculus42

Samuel Rouse

Posted on April 20, 2024

JavaScript: Timer Params

When you want a message to disappear after ten seconds or limit how long to wait for data, setTimeout and setInterval are probably the tools for the job. Small differences in how you use timers can create unexpected outcomes, but there are simple solutions.

Timer Basics

First off, you should know:

  1. Timers don't guarantee exact timing.
  2. Timers can behave differently in different browsers or environments.
  3. Timers have some history that limit their precision.

I've written about timer issues before, if you are interested in a deeper dive.

Basic Timers, Basic Bugs

Let's see a simple timer bug. Here are two nearly identical functions:

const one = () => {
  for (let i = 0; i < 5; i += 1) {
    setTimeout(() => console.log('one', i), i);
  }
}

const two = () => {
  let i;
  for (i = 0; i < 5; i += 1) {
    setTimeout(() => console.log('two', i), i);
  }
};

one();
two();
Enter fullscreen mode Exit fullscreen mode

If you run this – I recommend using RunJS for running code like this locally – you'll see the following:

'one' 0
'two' 5
'one' 1
'two' 5
'one' 2
'two' 5
'one' 3
'two' 5
'one' 4
'two' 5
Enter fullscreen mode Exit fullscreen mode

This output can be confusing. How did we get five over and over, and why are the fives mixed in? Shouldn't they all be at the end?

Block Scope

This is the difference in block scoping that ECMAScript 2015 introduced. In function one, we define i inside the for loop, so we get a distinct instance of i each loop. That instance is used immediately by setTimeout, and then later by console.log. Later is the key.

In function two, we defined i outside the for loop, so we have a shared value of i. setTimeout uses the value immediately, so we have timers for 1, 2, 3, and 4 milliseconds, but console.log accesses the value later, after the loop has completed and the value of i no longer passes the condition i < 5.

In this simple example, we can move the variable declaration and be done, but real-world use is often more complicated. Fortunately, there's a simple solution!

Timers, Parameters, and Closures

setTimeout and setInterval can pass arguments to their callbacks!

Rather than having to check the location of a variable declaration, we can just pass the value to setTimeout and consume it as a parameter in the callback.

const three = () => {
  for (let i = 0; i < 5; i += 1) {
    // setTimeout(callback, delay, param1, ...)
    setTimeout((value) => console.log('three', value), i, i);
  }
}

const four = () => {
  let i;
  for (i = 0; i < 5; i += 1) {
    setTimeout((value) => console.log('four', value), i, i);
  }
};

three();
four();
Enter fullscreen mode Exit fullscreen mode

Because this is passed to setTimeout directly and not accessed later by the function, it guarantees it uses the value available when setTimeout was called, even if the variable changes later.

'three' 0
'four' 0
'three' 1
'four' 1
'three' 2
'four' 2
'three' 3
'four' 3
'three' 4
'four' 4
Enter fullscreen mode Exit fullscreen mode
setTimeout(console.log, 1000, 'You can pass', 'multiple arguments', 'to the callback', 'after the delay argument');
Enter fullscreen mode Exit fullscreen mode

This can be useful when you need access to values from inside the calling function. We don't need a closure or anonymous function to provide access to variables when we can pass them directly. Knowing this, we can choose how we pass and access information.

const five = () => {
  let sum = 0;
  let i;
  for (i = 0; i < 5; i += 1) {
    setTimeout((val) => {
      sum += val;
      console.log(`val: ${val}, i: ${i}, sum: ${sum}`);
    }, i, i);
  }
}

five();
Enter fullscreen mode Exit fullscreen mode
'val: 0, i: 5, sum: 0'
'val: 1, i: 5, sum: 1'
'val: 2, i: 5, sum: 3'
'val: 3, i: 5, sum: 6'
'val: 4, i: 5, sum: 10'
Enter fullscreen mode Exit fullscreen mode

Closures & Reusable Functions

Because we can pass closure values through setTimeout, we don't have to depend on anonymous functions created in the closure scope.

const six = () => {
  for (let i = 0; i < 5; i += 1) {
    // setTimeout(callback, delay, param1, param2, ...)
    setTimeout(console.log, i, 'six', i);
  }
}

const seven = () => {
  let i;
  for (i = 0; i < 5; i += 1) {
    setTimeout(console.log, i, 'seven', i);
  }
};

six();
seven();
Enter fullscreen mode Exit fullscreen mode
'six' 0
'seven' 0
'six' 1
'seven' 1
'six' 2
'seven' 2
'six' 3
'seven' 3
'six' 4
'seven' 4
Enter fullscreen mode Exit fullscreen mode

You can also pass arrays and objects in these parameters, but you must be mindful of mutation and re-assignment of variables in these cases.

Conclusion

Using the optional parameters of timers makes it easier to re-use code and eliminate some complexity.

I hope you found this informational, or at least entertaining.

💖 💪 🙅 🚩
oculus42
Samuel Rouse

Posted on April 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related