Recognize what is stuck before changing the code

An infinite loop repeats without reaching its stopping condition. Runaway recursion repeatedly calls a function without reaching a base case. Both can make a page or program appear frozen, but they may fail differently: a loop might keep using CPU, while synchronous recursion often ends with a call-stack error. The exact error text depends on the JavaScript environment.

For the examples below, you need a browser developer console that supports JavaScript ES2015 or later. No packages are required. Do not paste a suspected infinite loop into the console just to see what happens. If a program is already stuck, try the debugger’s pause or stop control. If the browser is unresponsive, close the affected tab or stop its process; unsaved work in that tab may be lost.

First, narrow down the trigger. Does the problem start on page load, after a click, or only with a particular input? Does an error appear, or does execution continue without producing a result? Reproduce the smallest case you can. A small input and a known starting state make it easier to distinguish a termination bug from a task that is merely slow.

Check whether a loop can make progress

A loop needs a condition that can become false. Consider this JavaScript: let count = 0; while (count < 5) { console.log(count); }. The condition depends on count, but the body never changes count. Reading the condition and the variables it uses is usually more useful than staring at the repeated output. A corrected version increments count inside the body: let count = 0; while (count < 5) { console.log(count); count += 1; }.

When the update is present, check its direction and size. If a loop runs while index < items.length, does index increase on every path through the body? A continue statement can skip an update placed near the end. A variable might also be reset inside the loop, or an array might grow as quickly as the index moves through it. Write down the values of the condition’s variables for two or three iterations. If they do not move toward a stopping point, you have a specific place to investigate.

Avoid assuming that a changing value guarantees termination. For example, repeatedly adding a value that moves a counter away from its limit will not help. Identify a measurable distance to the stopping point, then check that each iteration reduces it under the inputs your program accepts.

Trace the base case and each recursive call

A recursive function needs both a base case and a step toward it. Here is a function for nonnegative integers: function countdown(n) { if (n === 0) return; console.log(n); countdown(n - 1); }. For countdown(3), the successive arguments are 3, 2, 1, and 0. At 0, the function returns without making another call.

Now imagine calling countdown(-1). Its argument becomes -2, then -3, and never equals 0. The base case exists, but it does not cover that input. You could reject negative values at the entry point, or choose a stopping condition appropriate to the intended behavior. Also check that every recursive branch moves toward a base case. A function with two recursive calls may make progress on one branch but repeat the same argument on the other.

When diagnosing recursion, record the input to each call rather than only the number of calls. A short sequence often exposes a repeated argument, an unexpected negative value, or a boundary the function steps past. A stack error is a clue, not proof that the only fix is a larger stack; the termination logic may be wrong.

Use the debugger and temporary guards to locate the cause

Set a breakpoint at the loop condition or the start of the recursive function. Inspect the variables that control termination, then step through a few iterations or calls. If the debugger supports conditional breakpoints, pause when a counter reaches a suspicious value. Logging can help too, but printing on every iteration may flood the console and make an already slow program harder to inspect.

A temporary iteration guard can keep an investigation manageable: let attempts = 0; while (shouldContinue()) { if (++attempts > 100) throw new Error('Iteration guard reached'); doWork(); }. The number 100 is a debugging limit, not a universal safe limit. For recursion, a temporary depth parameter or a call counter can serve the same purpose. A guard tells you where execution exceeded an expected bound; it does not by itself fix the condition that allowed it to happen.

After finding the cause, test the corrected code with a normal input, an input that should stop immediately, and a boundary input. Remove debugging guards that would reject valid work, or replace them with deliberate limits if the task genuinely needs a maximum. The goal is to explain why execution ends, not merely to make the symptom disappear.

Back to category ↗