Understanding Stale Closures In JavaScript
JavaScript
03/07/2021
Stale closures are difficult to avoid, therefore it's important to recognize its occurrence. In another article, I mentioned that closures are nothing more than the outer scope of a function.
However, there's one fine detail I left out: a closure resembles more a snapshot 📸 of the scope's data when the function is created. This means that there's a risk of accessing outdated data given the right circumstances.
An example
Things are always best explained through an example, aren't they?
let number = 0
const increment = () => { number += 1 const message = `Incremented to ${number}`
return () => { console.log(message) console.log(`Number: ${number}`) }}
const log = increment()increment()increment()log() // Incremented to 1; Number: 3
In this example, we have a function that increments a variable number
and at the same time returns an anonymous function that prints out 📝 the value of said variable.
Yet, something strange happens on the final line where log
is called: number
is printed out with 2 different values (i.e. 1
and 3
). One would instinctively think that it should be 3
in both cases.
What happened? In short: we are actually printing out and looking at two different number
variables. In long: read on. 😛
Understanding in steps
It's critical to understand what's happening in the following lines of code.
const log = increment()increment()increment()log()
Step 1
First off, the function increment
and variable number
are only ever created once in the entire program. However, anything declared inside of increment
is actually created every time 🔁 the function is run.
When increment
is first called, it:
- Updates
number
to 1, - Creates a new variable called
message
. This last variable copies the value of 1 fromnumber
and stores the string "Number: 1" in a certain location in memory, and - Creates and returns an anonymous function. During the former, said function establishes a reference to each variable outside its scope. This reference is maintained even after the parent function is called. This is closure!
Suppose we now call log
. This is what we get:
const log = increment()log() // Incremented to 1; Number: 1
Nothing unexpected so far. 😉
Step 2
const log = increment()increment()increment()log()
increment
is called 2nd time, it:
- Updates
number
to 2, - Creates a new variable called
message
. This variable copies the value of 2 fromnumber
and stores the string "Number: 2" in a new location in memory, and - Creates and returns a new anonymous function which references the location in memory of the newly created
message
.
By now, you hopefully understood that our anonymous 👤 function is referencing a different message
variable in memory whenever it's created.
const log = increment()log() // Incremented to 1; Number: 1const log2 = increment()log2() // Incremented to 2; Number: 2
Solving stale closure
So how would we go about solving this issue? For this example, there are 2 possibilities.
Either move message
to the root of the program, so that only one copy of it ever exists. It is then simply updated on every increment
call. Or... 🤔
let number = 0let message = ''
const increment = () => { number += 1 message = `Incremented to ${number}`
return () => { console.log(message) console.log(`Number: ${number}`) }}
const log = increment()increment()increment()log() // Incremented to 3; Number: 3
Move message
into the anonymous function. Sure, it will still be created on every function call. However, the difference is that it is created when log
is called, not increment
. Thus, it "sees" the latest value of number
as we intend it to.
let number = 0
const increment = () => { number += 1
return () => { const message = `Incremented to ${number}`
console.log(message) console.log(`Number: ${number}`) }}
const log = increment()increment()increment()log() // Incremented to 3; Number: 3