JavaScript’s Many Loops: A Field Guide

Loops are fundamental to programming—they let you repeat a block of work a set number of times, or until some condition flips. JavaScript offers more flavors of loops than many languages, and they each come with their own quirks and use cases. Here’s a practical look at what’s available, how they differ, and when you’d actually reach for one over another.

The while Family: Simple and Direct

The while loop is the most basic kind. You check a condition before every iteration, and the loop runs until that condition fails. It’s straightforward to read, often very fast, and the easiest way to accidentally write an infinite loop. You reach for while when you don’t know up front how many iterations you’ll need—you just keep going until a flag, counter, or external state says stop.

The do...while variant flips things around: the body executes once, then the condition is checked at the end of each pass. That guarantees at least one iteration, which can be handy when the loop body itself sets up the very condition you’re testing.

// remove the first item from an array and log it until the array is empty
let queue1 = ["a", "b", "c"];

while (queue1.length) {
  let item = queue1.shift();

  console.log(item);
}

// same as above but also log when the array is empty
let queue2 = [];

do {
  let item = queue2.shift() ?? "empty";

  console.log(item);
} while (queue2.length);

The Classic for Loop

The for loop is the standard tool when you know exactly how many times to run. Its three-part syntax—initializer, condition, increment—can look intimidating to beginners, but view it as a compact version of a while loop and it becomes easier to parse. If you want to execute something, say, ten times, or index through an array by position, for remains the idiomatic choice.

// log the numbers 1 to 5
for (let i = 1; i <= 5; i++) {
  console.log(i);
}

// same thing but as a while loop
let i = 1; // the first part of a for loop

// the second
while (i <= 5) {
  console.log(i);

  i++; // the third
}

("end");

Iterating with for...of

For reading through an array’s values, for...of is the clearest option. It works on any object that implements the iterable protocol—the built-in types include arrays, maps, sets, and strings, but you can make your own objects iterable too. To do so, you add a [Symbol.iterator] method that returns an iterator object. Think of an iterable as a factory that produces an iterator when asked. Generators, special functions marked with *, blur the line further by returning something that serves as both iterable and iterator at once.

let myList = {
  *[Symbol.iterator]() {
    yield "a";
    yield "b";
    yield "c";
  },
};

for (let item of myList) {
  console.log(item);
}

There’s an asynchronous counterpart to all this: async iterables, async iterators, and async generators, which you consume with for await...of.

async function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

// this time we're not making an iterable, but a generator
async function* aNumberAMinute() {
  let i = 0;

  while (true) {
    // an infinite loop
    yield i++;

    // pause a minute
    await delay(60_000);
  }
}

// it's a generator, so we need to call it ourselves
for await (let i of aNumberAMinute()) {
  console.log(i);

  // stop after one hour
  if (i >= 59) {
    break;
  }
}

One subtle but useful detail: for await...of handles regular (non-async) iterables without issue. The reverse doesn’t hold—you can’t plug an async iterable into a plain for...of loop.

let myList = ["a", "b", "c"];

for (let item of myList) {
  console.log(item);
}

Array Methods: forEach vs. map

Strictly speaking, forEach and map aren’t loops—they’re methods that iterate under the hood. Both have a shared history of being measurably slower than a hand-written for loop, and although the gap has narrowed, performance-sensitive code should still lean toward the classic forms.

These days, forEach has lost much of its reason to exist. With for...of available, you’d only choose forEach if you already have a standalone callback function sitting around, or if you need the item’s index (which forEach passes as its second argument). If you’re writing new code and aren’t constrained by an existing codebase’s conventions, skipping it is a reasonable call.

let myList = ["a", "b", "c"];

for (let item of myList) {
	console.log(item);
}

// but maybe if I need the index use forEach
["a", "b", "c"].forEach((item, index) => {
  console.log(`${index}: ${item}`);
});

map, by contrast, earns its place through readability: it transforms one array into a new one, item by item. It carries the same performance baggage as forEach, but its intent—pure transformation—reads clearly at a glance. That clarity is why you’ll see it everywhere in React and similar frameworks, where mapping over data to produce JSX elements is the standard pattern.

function MyList({items}) {
  return (
    <ul>
      {items.map((item) => {
        return <li>{item}</li>;
      })}
    </ul>
  );
}

The for...in Edge Case

No survey of JavaScript loops is complete without for...in, but it’s a tool you’ll likely use sparingly. It walks through an object’s enumerable keys—including those inherited through the prototype chain—which is exactly why many developers avoid it. That inherited-key behavior can lead to unexpected results unless you carefully guard with own-property checks.

For plain object literals, though, for...in is a legitimate way to enumerate keys. The old cross-browser worries about key ordering are gone: keys that look like array indices (positive integers) come first, in ascending numeric order, followed by everything else in their original insertion order.

let myObject = {
  a: 1,
  b: 2,
  c: 3,
};

for (let k in myObject) {
  console.log(myObject[k]);
}

Choosing the Right Loop

Loops are so routine that we rarely stop to question them. But JavaScript’s variety here isn’t redundancy—each construct answers a slightly different question. Repeating a known count points to for; iterating a collection’s values calls for for...of; waiting on async data wants for await...of; condition-based runs fall to while; and transforming whole arrays favors map. Knowing which one fits the situation will make your code clearer and keep it from working against you.