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: 3In 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
numberto 1, - Creates a new variable called
message. This last variable copies the value of 1 fromnumberand 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: 1Nothing unexpected so far. 😉
Step 2
const log = increment()increment()increment()log()increment is called 2nd time, it:
- Updates
numberto 2, - Creates a new variable called
message. This variable copies the value of 2 fromnumberand 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: 2Solving 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: 3Move 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