DEV Community

Nikolay Makhonin
Nikolay Makhonin

Posted on

A rare case of recursive looping without loops or stack overflow

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)