Why setTimeout Sees Stale Values: Five Closure Traps Every JavaScript Dev Hits
Async callbacks that read stale or unintended values cause bugs that pass code review because the code looks correct line by line. Knowing whether a closure captures a snapshot or a live reference determines whether a timer, event handler, or React effect behaves predictably under state changes.
A `for` loop with `var` prints the final value three times because the loop shares a single function-scoped binding. Switching to `let` gives each iteration its own block-scoped binding, fixing the output. The same mechanism causes stale state in React when `useEffect` or `useCallback` closes over a prop or state value without listing it in the dependency array.
Object properties accessed inside a callback are never snapshots; they resolve to whatever the property holds when the callback finally runs. To freeze a value, destructure it into a separate variable before the timer. When the goal is to read the latest value deliberately, keep the mutable reference alive in an outer scope.
Five concrete patterns cover the whole problem space: the `var` loop trap, IIFE-based value capture, intentional late reads, mutable object properties, and React's stale-closure bug with functional updates as the fix.
The distinction between capturing a reference and capturing a value is not a JavaScript quirk but a direct consequence of lexical scoping, and it trips up developers in every language with first-class closures and async execution.
React's stale-closure problem is the same `var`-loop trap in a different costume: a function holds onto a binding from a previous execution context, and the fix is either a new binding per render or a functional update that reads the latest state from the runtime.
The article's framing of 'snapshot vs. latest value' as an explicit design choice is more useful than treating one behavior as correct and the other as a bug; both are needed in different situations.