Avoiding While Loops for Safer Code
Tags: programming, java, beginners, codequality
Published: 2024-02-11 20:54:38

Looping constructs that do not specify an upper bound limit for repetition, such as the `while` loop, are the most susceptible to accidental infinite loops. Unintentionally triggering an infinite loop results in a process using increasing amounts of system resources until the process crashes, usually crashing other processes running on the same machine along with it. Using a `for` loop with incrementing conditions as a circuit breaker to avoid infinite loops is an effective alternative to `while` loops that more clearly communicates intent and protects production systems from runaway programs.

## Looping constructs in code

“Looping” in software is the ability to repeatedly execute a block of code until a particular condition is met. Most programming languages offer several different constructs for looping, such as `for`, `while`, `do-while`, and others. Some programming languages have no looping constructs at all, relying on recursion to repeatedly execute code. This discussion will focus on programming languages that _do_ have looping constructs.

All looping constructs are based around the concept that there is some block of code that needs to be executed repeatedly. The differentiator, beyond syntax and minor semantic details, is whether or not we know the maximum number of times that the code should be executed.

An example of a loop type where we do know the maximum number of times the loop should execute is the `for` loop. A `for` loop has up to three expressions in it’s definition:

- An **initializer** that is executed once before the loop begins. This usually is done to set some starting value that is incremented a fixed number of times to control the maximum number of times the loop can execute.

- A **condition expression** that is checked before each iteration of the for loop. If the condition evaluates to false, the loop stops executing.

- An **increment expression** that executes at the end of each iteration of the loop, before the next iteration begins. 

```JS
for (initializer; condition; increment){
  // body
}
```

The reason for saying “up to” three expressions is that most programming languages allow one or more of these expressions to be omitted. Most often, the for loop is used with all three expressions present.

## The While Loop
A common type of loop that is used when the number of times the loop needs to repeat is unknown is the `while` loop. The while loop definition consists of only a condition expression. Similarly to the condition expression of a for loop, this condition expression is evaluated before each iteration of the loop, and the loop stops executing once the condition evaluates to false.

```JS
while(condition){
  // body
}
```

`while` loops are commonly used when the number of times the loop needs to execute is not known ahead of time. For example, if we are traversing the nodes of a tree, we may not know exactly how many nodes are present in the tree, so a while loop may be used with a queue of tree nodes that executes until the queue is empty.

```JS
let queue = [startNode]; 
while(queue.length > 0) { 
  currentNode = queue.pop(); 
  if(currentNode){
    console.log(currentNode);
    queue.push(currentNode.left);
    queue.push(currentNode.right);
  }
}
```
  
## The Infinite Loop Problem

An infinite loop is the programmer’s tool to say “do this code over and over again until something purposely stops the program”. Intentional infinite loops, such as a web server listening for incoming requests, are designed in a way where the “infinite” part does not run out of control with system resources to avoid causing major problems.

Unintentional infinite loops, however, typically will consume ever-growing amounts of system resources until the process or thread ultimately crashes. These resource-hungry infinite loops have a large blast radius as well, causing other processes running on the same machine to crash as the infinite loops consumes all of the system resources.

## For Loops as a Circuit Breaker

The intention communicated by a while loop is that some code should be repeated until a condition is met, and the code should repeat, possibly forever, until that condition is satisfied. Most often, the more accurate intention of a developer is that the number of times the loop needs to run is not known, but it should stop before consuming all of the system resources and crashes.

A `for` loop can be used as an alternative to `while` loops to not only communicate this intent more clearly, but also avoid infinite loops entirely. To do this, we

- specify an upper bound limit for how many iterations the loop can repeat. This is usually an arbitrary limit found by trial and error.

- write our loop as a `for` loop with all three expressions. The initializer expression initializes a counter. The condition expression is our usual expression AND the counter has not exceeded the upper bound limit we specified. The increment expression increments the counter variable we initialized for this loop.

- After our loop, check that the end condition was satisfied, and gracefully handle the situation if the maximum threshold was exceeded. “Gracefully” should, at a minimum, include logging the fact that the loop reached the maximum threshold, what the threshold was configured to at the time, and any relevant inputs that can be used to investigate the cause for hitting the upper limit of loop executions. 

We can take the tree traversal code from earlier and re-write it as a `for` loop with an upper bound threshold.

```JS
let queue = [startNode]; 
let MAX_NODES_ALLOWED = 1000;

for (let nodesVisited = 0; 
     nodesVisited < MAX_NODES_ALLOWED && queue.length > 0;
     nodesVisited++;) { 
  currentNode = queue.pop();
  if(currentNode){
    console.log(currentNode);
    queue.push(currentNode.left);
    queue.push(currentNode.right);
  }
}

if(queue.length > 0){
  console.error("too many nodes to process");
}
  
```
Let’s examine the scenario where a tree has a cycle in it’s graph instead of behaving according to the rules of a valid binary tree. 

```JS
let a = {value: "A", left: undefined, right: undefined};
let b = {value: "B", left: undefined, right: undefined};
let c = {value: "C", left: undefined, right: undefined};
a.left = b;
a.right = c;
// causes a cycle here
b.left = a; 

let startNode = a;
```

Stepping through the code in the `while` loop implementation, we can see that the state of the queue after each iterations is as follows

```
[A]
[B, C]
[C, A]
[A]
[B, C]
...
```

The `while` loop will keep pushing and popping the same nodes to the queue until something kills the process. Usually, this will result in some messy error messages about running out of memory or processes timing out, without providing much context about what actually happened. It is highly unlikely that we intended to process the tree nodes infinitely until the system crashed.

In contrast, the `for` loop implementation put an upper bound limit of 1000 iterations of the loop. After the loop hits 1000 iterations, it stops repeating and logs an error message. This is a much better alternative that

- doesn’t crash the current process

- doesn’t cause other processes on the machine to crash

- gives meaningful logs for debugging the actual problem

The example code logs a simple message, but production code should log more meaningful context about what inputs and data caused the loop to exceed the upper bound threshold.

## Conclusion
High quality code communicates intent. The intent communicated by `while` loops is that code should be repeated, possibly forever, until a condition is met. Most often, the real intent is that the code needs to repeat for an unknown amount of iterations, but probably should stop before crashing the entire process. Using a `for` loop as a circuit breaker is an effective way to both avoid the possibility of infinite loop conditions and communicate an accurate intent to others reading the code.