Loops that never end
A loop is infinite when nothing ever makes it stop. The program keeps repeating the body, never reaches the lines after the loop, and keeps using memory or processor time until someone stops it. Usually that is a bug.
A for...of loop over an array looks safe, since it has no condition to get wrong: it ends when it runs out of elements. That safety holds only while the array has a fixed size. If the body keeps adding elements to the array the loop is walking, the end keeps moving away.
How adding to the array you walk over goes wrong
let fruits: string[] = ["banana", "orange"];
for (let fruit of fruits) {
fruits.push("kiwi");
}
A for...of loop over an array checks, before each pass, whether there is another element after the one it just handled. It checks against the array as it is at that moment, not against a size recorded when the loop started. Here is the flow:
- Pass 1 handles
"banana"and pushes a kiwi. The array now has three elements. - Pass 2 handles
"orange"and pushes another kiwi. Four elements. - Pass 3 handles the first kiwi, which is the third element, and pushes yet another. Five elements.
- Every pass moves one position to the right, and every pass adds one element on the right. The loop is always one step behind the end and never reaches it.
Nothing is reported while this happens, because the code is valid. The array grows until the program is stopped or runs out of memory, which is why the lesson calls it a loop that never makes it to the end.
The exercise
The task is to add every name from newStudents to students and then print all six names. The code you start with reads:
let students: string[] = ["Andy", "Mary", "Alex"];
let newStudents: string[] = ["Ana", "Alexa", "Mike"];
for (const student of newStudents) {
newStudents.push(student);
}
for (const student of students) {
console.log(student);
}
The loop walks over newStudents, and its body pushes onto newStudents too. It is the pattern from the example above: each pass adds an element to the array that is being walked, so the first loop never ends, and the printing loop is never reached.
The intent is to read from one array and write to another. Reading and writing the same array is what causes trouble.
- Keep
for (const student of newStudents)as the loop header. It reads each new name. - In the body, push onto
students, notnewStudents. The array being walked now stays at three elements, so the loop ends after three passes. - Leave the second loop alone. It prints every element of
students, which now has six.
let students: string[] = ["Andy", "Mary", "Alex"];
let newStudents: string[] = ["Ana", "Alexa", "Mike"];
for (const student of newStudents) {
students.push(student);
}
for (const student of students) {
console.log(student);
}
Andy
Mary
Alex
Ana
Alexa
Mike
Safe ways to change an array during a loop
- Write to a different array, as above. This is the cleanest fix when you are collecting results.
- Loop over a copy. Spreading makes a shallow copy that has a fixed size, so the original can grow safely:
for (const fruit of [...fruits]) { fruits.push(fruit); }doubles["banana", "orange"]into["banana", "orange", "banana", "orange"]and stops. - Record the length first. With a counting loop,
const len = vals.length;before the loop andi < lenin its condition fix the number of passes, whatever happens to the array inside.
Removing elements while looping is a quieter version of the same problem. If a body calls shift() on the array being walked, the elements slide left under the loop and one is skipped: looping over [1, 2, 3, 4] and removing the first element during the first pass makes the loop see 1, 3 and 4, and never 2.
Common mistakes
- Pushing to the wrong array. The two names are similar, and a loop over one with
pushon the other is easy to type by accident. Read the header and the body together. - Expecting the loop to use the starting size. It does not remember how long the array was when it began.
- Testing a possibly infinite loop on real data. If you suspect a loop might not end, try it with a small guard, for instance a counter that stops with
breakafter a few passes. - Thinking
constprevents this.conststops you from reassigning the variable, not from changing the array it refers to, sopushis still allowed.
Key takeaways
- A
for...ofloop on an array ends only when it reaches the end of the array as it currently is. - Adding an element to the array in its own loop keeps the end out of reach, and the loop never finishes.
- Read from one array and write to another, or loop over a copy.
- Removing elements during a loop can skip elements.