AuthonAuthon Blog
debugging6 min read

Why Your Loop Runs Forever (and How to Actually Debug It)

Walk through the most common causes of infinite loops, from off-by-one errors to floating point traps, with step-by-step debugging techniques and prevention patterns.

AW
Alan West
Authon Team
Why Your Loop Runs Forever (and How to Actually Debug It)

We've all been there. You write a loop that looks perfectly reasonable, hit run, and watch your terminal freeze, your CPU spike to 100%, or your browser tab crash. Infinite loops are one of those bugs that feel embarrassing once you find the cause — but tracking them down can be genuinely tricky.

Let me walk through the most common reasons loops go rogue, how to actually debug them when they do, and some patterns to prevent them from happening in the first place.

The Obvious Ones (That Still Get Us)

Before we get into the weird stuff, let's knock out the classics. Off-by-one errors and mutation mistakes account for probably 80% of accidental infinite loops I've seen in the wild.

javascript
// Classic: forgetting to increment
let i = 0;
while (i < 10) {
  console.log(i);
  // Oops — i never changes. This runs forever.
}

// Classic: mutating the wrong variable
for (let i = 0; i < items.length; i++) {
  items.push(processItem(items[i])); // array grows every iteration
}

The second one is sneaky. You're iterating over an array and pushing to it at the same time, so items.length keeps increasing. Your loop condition never becomes false. I ran into a variant of this last month where a queue processor kept re-adding failed items without a retry limit. Took down a staging server for twenty minutes before anyone noticed.

The Actually Interesting Ones

Here's where it gets fun. Some infinite loops aren't caused by forgetting to increment a counter — they're caused by subtle logic that creates a cycle.

Circular References in Object Traversal

If you're recursively walking an object or graph structure, circular references will send your function into an infinite loop (or blow the stack with infinite recursion, which is the recursive cousin of the same problem).

javascript
const a = { name: 'node-a' };
const b = { name: 'node-b', parent: a };
a.child = b; // circular reference: a -> b -> a -> b -> ...

// This will never terminate
function printAll(node) {
  console.log(node.name);
  for (const key of Object.keys(node)) {
    if (typeof node[key] === 'object' && node[key] !== null) {
      printAll(node[key]); // infinite recursion due to cycle
    }
  }
}

The fix is to track visited nodes:

javascript
function printAll(node, visited = new Set()) {
  if (visited.has(node)) return; // break the cycle
  visited.add(node);

  console.log(node.name);
  for (const key of Object.keys(node)) {
    if (typeof node[key] === 'object' && node[key] !== null) {
      printAll(node[key], visited);
    }
  }
}

A Set works here because it checks by reference identity. If you're dealing with serialized data (like JSON with IDs), you'd use a Set of IDs instead.

Floating Point Loops That Never Converge

This one bites people in every language.

python
# This might never terminate depending on your platform
x = 0.0
while x != 1.0:
    x += 0.1
    print(x)

Because 0.1 can't be represented exactly in IEEE 754 floating point, adding it ten times doesn't give you exactly 1.0. It gives you something like 0.9999999999999999 or 1.0000000000000002. The != check never passes.

The fix: never use exact equality with floats in loop conditions. Use a threshold instead.

python
x = 0.0
while x < 1.0 - 1e-9:  # small epsilon for safety
    x += 0.1
    print(round(x, 1))

Or better yet, use integer counting and derive the float value:

python
for i in range(10):
    x = (i + 1) / 10  # derive from integer — always exact logic
    print(x)

State Machine Loops

If you're implementing any kind of state machine — a parser, a game loop, a workflow engine — you can accidentally create states that cycle without making progress.

python
state = "retry"
while state != "done":
    if state == "retry":
        result = attempt_operation()
        if not result.success:
            state = "retry"  # never transitions out if it keeps failing
        else:
            state = "done"

No max retry count means this runs forever if the operation keeps failing. Always add a bound.

How to Debug a Loop That's Already Stuck

When your program is frozen and you're staring at a spinning cursor, here's what to do.

1. Don't Kill It Immediately

If you can, attach a debugger first. In Node.js, you can send SIGUSR1 to a running process to activate the inspector:

bash
# Find the PID
pgrep -f "node your-script.js"

# Activate inspector on a running process
kill -SIGUSR1 <pid>
# Then open chrome://inspect in Chrome

In Python, if you planned ahead and included faulthandler, you can dump the traceback with SIGABRT.

2. Add a Safety Counter

When you suspect a loop might be infinite but aren't sure, wrap it with a safety valve:

javascript
let safety = 0;
const MAX_ITERATIONS = 100_000;

while (someCondition) {
  if (++safety > MAX_ITERATIONS) {
    console.error('Loop exceeded max iterations', {
      // Log whatever state matters
      currentIndex,
      lastProcessedItem,
    });
    break;
  }
  // ... rest of loop
}

This is a debugging technique, not a permanent fix. The logged state usually tells you exactly why the loop isn't terminating.

3. Binary Search Your Logic

If the loop is in complex code, comment out half the loop body, run it again. Still infinite? The bug is in the remaining half. Not infinite anymore? The bug was in what you removed. Repeat. It's crude but effective when print debugging gets overwhelming.

Prevention Patterns

A few habits that have saved me from infinite loops over the years:

  • Prefer for over while when possible. A for loop with a clear bound is harder to make infinite. while (true) with a break condition is the most dangerous pattern.
  • Always add retry limits. Any loop that retries an operation needs a max attempt count. No exceptions.
  • Use Set for graph traversal. If you're walking any structure that could have cycles, track visited nodes from the start — don't add it after you hit the bug.
  • Avoid float equality in conditions. Use <, >, or threshold-based comparisons.
  • Log iteration counts in production. If a loop runs more than you expect, you want to know about it before it becomes an incident. A simple metric or log line at every N iterations goes a long way.

The Takeaway

Infinite loops are fundamentally a termination problem — your loop's exit condition never becomes true. When debugging, focus on why the condition stays false rather than trying to trace every iteration. Check what's supposed to change each iteration and verify that it actually does.

And honestly, there's no shame in hitting one. I've shipped infinite loops to production more than once. The difference between a junior and senior dev isn't that seniors never write them — it's that seniors add the safety valves and monitoring that catch them before users do.

Why Your Loop Runs Forever (and How to Actually Debug It) | Authon Blog