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?

JAVASCRIPT
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.

JAVASCRIPT
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 from number 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:

JAVASCRIPT
const log = increment()
log() // Incremented to 1; Number: 1

Nothing unexpected so far. 😉

Step 2

JAVASCRIPT
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 from number 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.

JAVASCRIPT
const log = increment()
log() // Incremented to 1; Number: 1
const 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... 🤔

JAVASCRIPT
let number = 0
let 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.

JAVASCRIPT
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

WRITTEN BY

Code and stuff