Why a closure doesn’t see reassigned parameters

Consider a function that returns a logger closing over a variable passed in:

function getLogger(arg) {
	function logger() {
		console.log(arg)
	}
	return logger
}

let fruit = 'raspberry'
const logFruit = getLogger(fruit)

logFruit() // "raspberry"
fruit = 'peach'
logFruit() // "raspberry" Wait what!? Why is this not "peach"?

fruit starts as 'raspberry', so the first call logs that string. But after reassigning fruit to 'peach', calling the same logger still logs 'raspberry'. The only way to get the new value is to call getLogger again:

const logFruit2 = getLogger(fruit)
logFruit2() // "peach" what a relief...

The reason is that JavaScript passes function arguments by value, not by reference. When getLogger(fruit) executes, the engine creates a new parameter variable and copies the current value of fruit into it:

function getLogger(arg) {
	function logger() {
		console.log(arg)
	}
	return logger
}

// side-note, this could be written like this too
// and it wouldn't make any difference whatsoever:
// const getLogger = arg => () => console.log(arg)
// I just decided to go more verbose to keep it simple

Variables are arrows to memory, not aliases

Viewing a variable as an arrow pointing to a location in memory clarifies what happens. let a = 1 creates a memory slot holding 1 and points a at it. Then let b = a creates a separate arrow b pointing at the same location at that moment; it does not link b to a.

This simple example shows the effect:

let a = 1
let b = a

console.log(a, b) // 1, 1

a = 2
console.log(a, b) // 2, 1 ‼️

Changing what a points to later leaves b unaffected, because they are independent variables that merely happened to share a value initially.

The same logic applies to the function call. Under the hood, calling getLogger(fruit) is roughly equivalent to:

let arg = fruit

So when fruit = 'peach' runs later, it only redirects the outer fruit arrow. The closure’s arg variable still points to 'raspberry'.

Keeping two variables in sync

If you genuinely need the logger to reflect updates, the workaround is to avoid reassigning variables entirely. Instead, mutate the value they share. If two variables point to the same object, changing a property on that object is visible through both:

let a = { current: 1 }
let b = a

console.log(a.current, b.current) // 1, 1

a.current = 2
console.log(a.current, b.current) // 2, 2 🎉

Applying that pattern to the logger problem:

function getLatestLogger(argRef) {
	function logger() {
		console.log(argRef.current)
	}
	return logger
}

const fruitRef = { current: 'raspberry' }

const latestLogger = getLatestLogger(fruitRef)

latestLogger() // "raspberry"
fruitRef.current = 'peach'
latestLogger() // "peach" 🎉

The Ref suffix stands for “reference”: the variable’s value is an object used only to hold another value (here the current property). Reassigning current mutates the shared object, so the closure sees every update.

Trade-offs

The pass-by-value rule is a deliberate part of the language specification. Reassigning parameters or outer variables inside a closure won’t propagate back to the caller. When you do need shared, mutable state, the object-wrapping pattern works—though it’s rarely necessary, since mutability tends to make programs harder to reason about. The workaround is straightforward when the need does arise.