Hanging is one of the trickiest bugs to debug. Knowing the possible causes helps you spot them faster.
Minimal case to reproduce the hang
Set N = 64, and this function will never finish. The number of calls will be 2^(N+1), while the maximum stack depth stays at just N.
let calls = 0
const N = 18
function func(state, visited) {
calls++
if (calls > 10_000_000) {
throw new Error('calls: ' + calls)
}
if (visited.includes(state)) return
const newVisited = [...visited, state]
func((state + 1) % N, newVisited)
func((state + 1) % N, newVisited)
}
func(0, [])
console.log('calls:', calls)
Why does this work without stack overflow?
func(0, [])
├── func(1, [0])
│ ├── func(2, [0,1])
│ │ └── ... depth grows to N,
│ │ all combinations of newVisited are iterated
│ └── func(2, [0,1]) — returns, depth DECREASES
└── func(1, [0]) — second call, stack already freed
Meanwhile, the Garbage Collector keeps removing previously created newVisited arrays.
The stack "breathes" — it reaches the maximum depth of N, then collapses back, then grows again. This is essentially traversing a huge tree that has small depth but enormous width.
This is not infinite recursion. But with N = 64, the number of calls will be 2^65 (approximately 10^19) — that would take thousands of years to complete, and the stack will never overflow.
Top comments (0)