DEV Community

Jan Küster
Jan Küster

Posted on

Prevent infinite loops in JavaScript

There are some typical beginner (YES, AND EXPERT-) errors in JavaScript that can be a real pain: infinite loops ♾️

They occur, when the control structures have no condition to end (branch out) and thus execute indefinitely.

This guide covers some causes of infinite loops and how to prevent them.

1. The no-brainer
2. The forgotten increment / decrement
3. The uncontrollable source of truth
4. The unintended override
5. The unorthodox breaking condition
6. More options to prevent infinite loops



1. ♾️ The no-brainer

If you never have encountered an infinite loop, simply run the following code. Do not use your developer console to directly execute code but use online tools like CodePen, otherwise you may have to force-quit the whole browser process (depending on your os, browser and version):

while (true) {
  console.log('pleas wait just one more second')
}
Enter fullscreen mode Exit fullscreen mode

or even shorter:

while (true);
Enter fullscreen mode Exit fullscreen mode

That's a nasty experience and when one of your users gets into this situation, you can be sure you have just lost her.

How to prevent

Run brain.exe before coding. I think the vast majority will not face this one unless intentionally done.

If this really happens to you by accident: it's time to go home for today.



2. ♾️ The forgotten increment / decrement

This is a classic one and even experienced developers fall into it from time to time, especially when working too long and concentration(tm) has left the chat:

let i = 0

while (i < 10) {
  console.log(i)
  // i++ is missing
}
Enter fullscreen mode Exit fullscreen mode

Similar with decrementing:

let i = 9

while (i >= 0) {
  console.log(i)
  // i-- is missing
}
Enter fullscreen mode Exit fullscreen mode

Prevent it using auto-increment / auto-decrement

For beginners this might be confusing, due to way how pre-increment and post-increment (and *-decrement) work.

Suggestion: read it up first and then use it directly in your while conditional:

let i = 0

while (i++ < 10) {
  console.log(i) // will be 1,2,3,4,5,6,7,8,9,10
}
Enter fullscreen mode Exit fullscreen mode

As you can see, this will not count i from 0 ... 9 so we need to fix the indices:

let i = -1

while (i++ < 9) {
  console.log(i) // will be 0,1,2,3,4,5,6,7,8,9
}
Enter fullscreen mode Exit fullscreen mode

Yes, I know it becomes rather confusing than it helps. This is because the i is incremented before the body is executed (as opposed to for loops, where it will be incremented after the body has been executed). Just keep it in mind when you next time design a while loop with auto-increment.

With pre-increment (already range-corrected):

let i = -1

while (++i <= 9) {
  console.log(i) // will be 0,1,2,3,4,5,6,7,8,9
}
Enter fullscreen mode Exit fullscreen mode

A good exercise in between: Implement the same while loop with auto pre-decrement (--i) and auto post-decrement (i--).



3. ♾️ The uncontrollable source of truth

Sometimes you use while loops to do some operation until a condition is met and where the breaking condition is not based on numerical values.

If the source of this condition is hardly determinable (as opposed to counters with a numerical limit) you may face infinite loops. The worst: in rare cases these may occur only in a few situations to a few users and debugging sessions will be long and exhaustive!

let ended = false

while (!ended) {
  // do stuff
  ended = getIsEnded() // ticking time-bomb
}
Enter fullscreen mode Exit fullscreen mode

Use a safety counter to prevent this

If you really can't redesign this one towards a more determined condition then you may introduce some kind of safetey-counter.

This counter will be the super-most-upper-maximum of iterations that run and if it's reached you expect the loop to run into infinity mode and throw an Error to prevent this:

let ended = false
let safety = 0
const maxSafety = 1000

while (!ended && safety++ < maxSafety) {
  // do stuff
  ended = getIsEnded() // just tick...
}

if (!ended) {
  throw new Error('Infinite loop detected and prevented')
}
Enter fullscreen mode Exit fullscreen mode



4. ♾️ The unintended override

Let's say your code gets more and more complex and you will face situations, where your counter or condition is overridden or altered then you may not realize that this may lead into infinite loops:

const array = [0,1,2,3]
for (let i = 0; i < array.length; i++) {
  // do stuff...
  array.push(-1) // boom
}
Enter fullscreen mode Exit fullscreen mode

Another example:

const obj = { count: i, max: 10 }
const increment = obj => {
  obj.count++
  obj.max++ // unintended but still boom
}

while (obj.count < obj.max) {
  // do stuff
  increment(obj)
}
Enter fullscreen mode Exit fullscreen mode

While this example is somewhat exotic and I suggest to never do such constructs it shows that some of JavaScript's features (pass object by reference) used the wrong way can easily cause trouble.

Prevent using immutable maximum

Using a const for maximum values makes it much harder to manipulate the upper bounds:

const array = [0,1,2,3]
const length = array.length

for (let i = 0; i < length; i++) {
  // do stuff...
  array.push(-1) // who cares
}
Enter fullscreen mode Exit fullscreen mode

Some goes for the while loop:

const max = 10
const obj = { count: 0 }
const increment = obj => {
  obj.count++
}

while (obj.count < max) {
  // do stuff
  increment(obj)
}
Enter fullscreen mode Exit fullscreen mode

However, just don't use this second example at all and better rewrite your code to use independent variables:

const max = 10
let i = 0

while (i < max) {
  // do stuff
  i++
}
Enter fullscreen mode Exit fullscreen mode



5. ♾️ The unorthodox breaking condition

You can create some crazy complex conditions for breaking loops. This could also potentially cause infinite loops.

Consider a loop that breaks only if the counter is exactly a specific value (as opposed to using less-than or greater-than):

for (let i = 0; i !== 5; i++) {
  console.log(i) // 0,1,2,3,4
}
Enter fullscreen mode Exit fullscreen mode

Yes it works and breaks as expected. But what if your counter is not incremented using the ++ operator but, say using += 3?

for (let i = 0; i !== 5; i += 3) {
  console.log(i) // 0,3,6,9,12,15...
}
Enter fullscreen mode Exit fullscreen mode

Prevention options

First you can introduce a safety counter (as shown before) or add a more determinable condition:

for (let i = 0; i !== 5 && i < 10; i += 3) {
  console.log(i) // 0,3,6,9,12,15...
}
Enter fullscreen mode Exit fullscreen mode

Try to avoid breaking conditions that introduce the possibility to never occur.



6. 🔧 More options to prevent infinite loops

Iterate over iterable

Iterables are great as they can be safely iterated via for..of and never cause infinite loops when only reading:

for (const num of [0,1,2,3,4]) console.log(num) // 0,1,2,3,4
for (const char of 'hello') console.log(char) // h,e,l,l,o
for (const name of new Set(['jane','john'])) console.log(name) // jane, john
Enter fullscreen mode Exit fullscreen mode

Note, however, that extending / altering the structures during the loop will still be a potential cause for infinity loops!

Use a safe for-loop

The easiest way to prevent the loop is to use conditions that are always determined. The following for-loops are very good examples of that:

Iterate n-times in forward direction

for (let i = 0; i < 10; i++) { ... }
Enter fullscreen mode Exit fullscreen mode

Iterate n-times in backwards direction

for (let i = 9; i >= 0; i--) { ... }
Enter fullscreen mode Exit fullscreen mode

In both cases the loops will always run through (unless you try to manipulate i inside the body but I assume you know that would be a very dangerous thing to do).

Use a function with "safe-iteration"

You can write a function, that implements a loop in a safe way and which executes a given function in each step:

const loop = ({ n, fct, backwards }) => {
  let i

  if (backwards) {
    for (i = n - 1; i >= 0; i--) fct(i)
  }

  // default mode is forward
  else {
    for (i = 0; i < n; i++) fct(i)
  }
}

// usage
loop({ n: 5, fct: i => console.log(i) }) // 0,1,2,3,4
loop({ n: 5, fct: i => console.log(i), backwards: true }) // 4,3,2,1,0
Enter fullscreen mode Exit fullscreen mode

Use a safe while-loop

The following function is an example of a while loop, wrapped in a more safe environment that will prevent infinite loops:

const safeWhile = ({ condition, fct, max = 1000 }) => {
  let i = 0
  let value // cover optional return value

  while (condition(i)) {
    if (i++ >= max) {
      throw new Error('Infinite loop detected and prevented')
    }
    value = fct(i)
  }

  return value
}

// usage
safeWhile({
  condition: i => true,
  fct: i => {}
})
// throws error but is never infinite
Enter fullscreen mode Exit fullscreen mode

Summary

I hope this collection of causes and fixes will help you to write more robust code and prevent these nasty infinite loops at all cost to maximize functionality and stability of your applications.

If you think there are concepts missing, confusing or simply wrong, please leave a comment so the article can be improved for everyone ❤️

Top comments (0)