JavaScript has four loop forms and they are not interchangeable. The classic for counts, while repeats until a condition changes, for...of walks a list, and for...in walks an object's keys — and confusing the last two is the single most common loop bug in JavaScript.
Which loop forms does JavaScript have?
| Form | Use it when |
|---|---|
for | You know how many times to repeat, or need an index |
while | You repeat until something changes, and the count is unknown |
do...while | Same as while, but the body must run at least once |
for...of | You want the values of an iterable — an array, a string, a Set |
for...in | You want the keys of an object. Almost never for arrays |
Pick the one whose shape matches the problem rather than the one you have typed before. That single habit removes most off-by-one errors before they happen.
How does the classic for loop work?
Three parts in the header, separated by semicolons: an initialiser that runs once, a condition checked before every iteration, and an update that runs after every iteration.
const days = ["Mon", "Tue", "Wed"];
for (let i = 0; i < days.length; i += 1) {
console.log(`${i + 1}. ${days[i]}`);
}
The output is three lines: 1. Mon, 2. Tue and 3. Wed.
The classic trap here is writing i <= days.length. The array has three items at indices 0, 1 and 2, so index 3 is undefined, and your loop prints a fourth line with nothing in it. Another common mistake is starting the index at 1 and using days[i], which skips the first element. When you use an index, remember that the first element is at index 0.
Declaring the counter with let inside the header is deliberate: it gives each loop its own counter and keeps it out of the surrounding scope.
When should I use while instead?
When the number of repetitions depends on something you cannot know in advance — a retry loop, a queue that drains as you process it, a poll that waits for a job to finish.
let remaining = 3;
while (remaining > 0) {
console.log("countdown:", remaining);
remaining -= 1;
}
let attempt = 0;
do {
attempt += 1;
console.log("attempt:", attempt);
} while (attempt < 2);
The countdown prints 3, 2 and 1. Then the do...while loop prints attempt 1 and attempt 2 — it checks the condition at the end of the body, so the body always runs at least once. That difference matters when the first pass is mandatory, such as validating a form before you know whether a second attempt is needed.
Here is the same do...while showing the difference:
let value = 10;
do {
console.log("ran with value:", value);
value += 1;
} while (value < 5);
It prints one line — ran with value: 10 — even though the condition is false from the start. A while loop with the same condition would print nothing at all.
Whatever you choose, make sure the loop terminates. A while (true) with no break inside it locks up the page and the browser will eventually offer to kill the tab.
What does for...of give me?
The values, in order, from anything iterable. It removes the index arithmetic entirely, which is why it replaced the classic for for most array work.
const languages = ["JavaScript", "Python", "Go"];
for (const language of languages) {
console.log(language);
}
for (const letter of "JS") {
console.log(letter);
}
The first loop prints the three language names. The second prints J and S, because a string is iterable too. Sets, Maps and the entries of an array are all iterable, so the same form works across them.
When you need the index as well, for...of does not give it to you directly, but you can get the position with the entries method of the array, which produces index-value pairs:
const days = ["Mon", "Tue"];
for (const [index, day] of days.entries()) {
console.log(index, day);
}
That prints 0 Mon and 1 Tue. The array module later in this tutorial covers the array methods in full, including the difference between for...of and forEach.
Why is for...in dangerous for arrays?
Because it gives you the keys, and for an array the keys are the string indices. They are strings, not numbers, and if the array has extra properties those come along too.
const scores = { ana: 91, ben: 78, cy: 84 };
for (const name in scores) {
console.log(`${name}: ${scores[name]}`);
}
const days = ["Mon", "Tue"];
for (const key in days) {
console.log(key, typeof key);
}
The first loop prints the three name-value pairs, which is exactly what you want for...in to do. The second prints 0 string and 1 string — the keys, as strings. The values "Mon" and "Tue" never appear, and you had to read them back out with days[key] to get them at all.
Use for...in for objects, for...of for arrays. That one rule removes an entire family of bugs, including the ones where a string index silently works in one place and fails in another.
How do break and continue work?
break leaves the loop immediately. continue skips the rest of the current iteration and goes back to the top.
const numbers = [1, 2, 3, 4, 5, 6];
for (const n of numbers) {
if (n % 2 === 0) continue;
console.log("odd:", n);
}
for (const n of numbers) {
if (n > 4) break;
console.log("before the break:", n);
}
The first loop prints odd: 1, odd: 3 and odd: 5 — the even numbers are skipped by continue. The second prints before the break: 1 through before the break: 4, then stops at 5 because of the break.
How do I break out of a nested loop?
A break only leaves the loop it is written in, so breaking out of two nested loops at once needs a label.
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
outer: for (const row of matrix) {
for (const value of row) {
if (value === 5) {
console.log("found 5, stopping");
break outer;
}
}
}
The label outer is attached to the outer loop, and break outer exits both. The output is a single line: found 5, stopping.
Labels are powerful and easy to abuse, so use them when a nested loop genuinely needs to escape, not as a general shortcut. If you find yourself labelling three levels deep, the problem is usually that the work should be extracted into a function that returns early instead.
How to choose a loop
- Known count or index needed: the classic
for. - Repeat until a condition changes:
while. Body must run at least once:do...while. - Values of an array, string, Set or Map:
for...of. - Keys of an object:
for...in. Never for an array. breakexits a loop,continueskips one iteration, and a label letsbreakexit nested loops.- Every loop needs a path to its condition becoming false. Make sure the update actually moves towards it.
Try it yourself: take a list of task names and write the same loop four ways — classic for with an index, for...of, forEach and map. Print each task with its position in the output. Then change the list to an object keyed by task id and see which of the four loops still makes sense. That experiment is the fastest way to internalise the difference between values and keys.