Say you want to print the numbers 1 to 100. You could write console.log(1), then console.log(2), and keep going for 98 more lines. Or you could write three lines that tell JavaScript: "do this again, and again, until I say stop." That's a loop. This page shows you exactly what happens on every lap of a loop, step by step, and the tiny mistake (being off by one) that trips up beginners and experts alike.

Repeat, with a counter

Here is the most common loop in JavaScript, the for loop. It prints a greeting three times:

for (let i = 0; i < 3; i++) {
  console.log("Hello #" + i);
}
// Hello #0
// Hello #1
// Hello #2

(console.log is JavaScript's way of printing a line of text. You can try any of this in your browser: press F12 and open the Console tab.)

The code between the curly braces { } is the loop's body: the part that gets repeated. Each run through the body is called an iteration, or a lap. The variable i is the counter. It's just an ordinary variable, and the name i (short for "index") is a tradition, not a rule. You could call it lap or n.

The line that starts with for has three parts inside the brackets, separated by semicolons:

1 · Setuplet i = 0
Runs once, before anything else. Creates the counter and gives it a starting value.
2 · Conditioni < 3
A yes/no question checked before every lap. If the answer is true, run the body. If false, the loop is over.
3 · Updatei++
Runs after every lap. i++ is shorthand for i = i + 1: add one to the counter.

The order is the part people get wrong in their heads, so step through it and watch. The orange box shows which piece of the code JavaScript is running right now.

Loop stepper

variables
condition check
not checked yet
console output

Notice the rhythm: check, body, update, check, body, update… The condition is asked one more time than the body runs. With i < 3, the body runs for 0, 1 and 2, and then the fourth check (3 < 3) is false, which ends the loop. That last "no" is what stops it, and it means the counter finishes at 3, a value the body never saw.

The while loop: repeat while something is true

A while loop is the same idea with the parts spread out. It only has the condition in its brackets. You do the setup before the loop and the update inside the body yourself:

let n = 3;               // setup
while (n > 0) {          // condition
  console.log(n);
  n--;                    // update: n = n - 1
}
console.log("Liftoff!");
// 3, 2, 1, Liftoff!

Pick the while loop tab in the stepper above to see it run. Every for loop can be rewritten as a while loop and the other way round. So which one should you use?

The infinite loop. If the condition never becomes false, the loop never ends. Delete the n--; line above and n stays 3 forever, so 3 > 0 is always true. Your browser tab freezes, and after a while it offers to stop the page. Every loop needs something in it that moves the counter toward the finish line.

The off-by-one bug

An off-by-one error is when a loop runs one time too many or one time too few. It's so common it has its own name, and it almost always comes from two facts that pull against each other.

Fact one: JavaScript counts positions in a list (an array) starting from 0. In ["kiwi", "fig", "plum"], "kiwi" is at position (or index) 0 and "plum" is at index 2.

Fact two: the array's .length is how many items it has, which is 3. So the last index is always length - 1, never length.

Now look at this loop. Its author wanted to print every fruit:

const fruits = ["kiwi", "fig", "plum"];
for (let i = 0; i <= fruits.length; i++) {
  console.log(fruits[i]);
}
// kiwi
// fig
// plum
// undefined   ← one lap too many

The <= ("less than or equal to") lets i reach 3, and fruits[3] doesn't exist. JavaScript doesn't stop with an error here; it quietly hands back undefined, its value for "nothing here". That makes the bug easy to miss. Step through the off-by-one bug tab above and watch the moment it happens.

The fix is one character: i < fruits.length. A handy rule for counting laps:

Both are fine. What matters is that the start and the comparison match each other. The two safe habits: start at 0 and use < (perfect for arrays), or start at 1 and use <= (good for "count from 1 to 10"). Mixing them, 0 with <= or 1 with <, is where off-by-one bugs come from.

Loop playground

Change the start, end and step, and flip the comparison. The playground runs the loop for you and shows every value i takes, plus the value that made the condition false (the dashed red one). Try the presets, especially the last three.

Loop playground

0
5
1
compare with

  

A few things worth noticing in the playground:

Check yourself

Predict the answer first, then click.

How many times does this print "hi"?

for (let i = 0; i < 4; i++) {
  console.log("hi");
}

The body runs for i = 0, 1, 2 and 3. Then 4 < 4 is false. Starting at 0 with <, the lap count is simply the end number.

What is the last number printed?

for (let i = 1; i <= 10; i += 3) {
  console.log(i);
}

i goes 1, 4, 7, 10, and 10 <= 10 is true, so 10 is printed. Next i becomes 13, 13 <= 10 is false, and the loop ends without printing 13.

What does this print?

let x = 5;
while (x > 5) {
  console.log(x);
  x--;
}

The condition is checked before the first lap. 5 > 5 is false, so the body never runs. To include 5, the condition would need x >= 5, or more likely x > 0.

What does this print?

const colors = ["red", "green"];
console.log(colors[colors.length]);

colors.length is 2, but the indexes are 0 and 1. There's nothing at index 2, and JavaScript returns undefined rather than an error. The last item is colors[colors.length - 1].

The short version

Next time a loop prints one item too many, or one too few, you'll know where to look first: the start value and the comparison sign.