Fixing a Stale Closure Bug in a JavaScript Timer
A common React/JS bug where a setInterval keeps using old state — here's the problem, why it happens, and how to fix it.
Muhammad Ali
MERN Stack developer
A common React/JS bug where a setInterval keeps using old state — here's the problem, why it happens, and how to fix it.
The Problem
I was building a simple counter that updates every second using setInterval. Here's the buggy code:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // 🐛 bug is here
}, 1000);
return () => clearInterval(interval);
}, []); // empty dependency array
return <div>{count}</div>;
}
At first glance this looks fine. But the counter gets stuck at 1 and never increases past that, no matter how long you wait.
Why This Happens
This is a classic stale closure problem.
When useEffect runs once (because of the empty [] dependency array), the setInterval callback captures the value of count at that exact moment — which is 0. Every time the interval fires, it's still using that original count = 0 from the closure, so it always does setCount(0 + 1), over and over. The state itself updates, but the closure never "sees" the new value.
The Solution
There are two clean ways to fix this.
Option 1: Use the functional update form
```js useEffect(() => { const interval = setInterval(() => { setCount(prev => prev + 1); // ✅ always uses latest state }, 1000);
return () => clearInterval(interval); }, []); ```
Instead of referencing count directly, we use the functional updater prev => prev + 1. React guarantees prev is always the most current state value, regardless of what the closure captured.
Option 2: Add count to the dependency array
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, [count]); // re-creates
```interval every time count changes
This works too, but it's less efficient — it tears down and recreates the interval on every single render, which isn't ideal for something like a timer.
## Takeaway
When working with `setInterval`, `setTimeout`, or any callback inside `useEffect`, always prefer the **functional update form** of `setState` if you need the latest value. It avoids stale closures without needing to over-populate your dependency array.
Written by
Muhammad Ali
MERN Stack developer