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:
- Timers don't guarantee exact timing.
- Timers can behave differently in different browsers or environments.
- 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();
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
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();
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
setTimeout(console.log, 1000, 'You can pass', 'multiple arguments', 'to the callback', 'after the delay argument');
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();
'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'
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();
'six' 0
'seven' 0
'six' 1
'seven' 1
'six' 2
'seven' 2
'six' 3
'seven' 3
'six' 4
'seven' 4
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.
Top comments (0)