Loops — for, while, and for...of

8 min read

QA work runs the same logic over many things — an array of test users, the rows of a fixture file, the items of an API response, the retries of a flaky request. Loops are how JavaScript repeats a block of code without copy-paste. This lesson covers the three forms you'll meet (for, while, for...of), the break and continue controls that interrupt them, and the infinite-loop trap to watch out for.

Why loops matter for QA

Three patterns dominate test code:

  • Iterate test data. Run the same test against many inputs — admins, members, anonymous, banned users.
  • Process API responses. Walk through a JSON array of results and assert on each.
  • Retry on flake. Re-issue a request until it succeeds or you've tried too many times.

Each one is a loop. Knowing which form to reach for is the difference between code that reads at a glance and code you have to puzzle out.

The classic for loop

The for loop has three parts inside the parentheses, separated by semicolons:

for (initialiser; condition; increment) { ... }
  • Initialiser runs once before the loop starts.
  • Condition is checked before every iteration; the loop stops when it becomes false.
  • Increment runs at the end of every iteration.
for (let i = 0; i < 3; i++) {
  console.log(`Run ${i + 1}`);
}

Output:

Run 1
Run 2
Run 3

let i = 0 runs once. The body runs while i < 3. After every body, i++ adds one to i. When i becomes 3, the condition is false and the loop ends.

Use a classic for when you need the index — for example, "log the position of each test case in the suite":

const testCases = ["login", "checkout", "search"];
 
for (let i = 0; i < testCases.length; i++) {
  console.log(`#${i + 1}: ${testCases[i]}`);
}

Output:

#1: login
#2: checkout
#3: search

for...of — the modern, clean form

When you don't need the index, for...of is shorter and reads better. It walks each value of an iterable (most commonly an array).

const users = [
  { name: "Alice", role: "admin" },
  { name: "Bob",   role: "member" },
  { name: "Carol", role: "guest" }
];
 
for (const user of users) {
  console.log(`${user.name} → ${user.role}`);
}

Output:

Alice → admin
Bob → member
Carol → guest

const user is rebound to the next array item each iteration. No counter, no length check, no off-by-one risk. For 90% of QA loops over arrays, this is the right tool.

while — when iterations aren't known up front

while keeps running as long as a condition is true. Unlike for, the iteration count isn't known up front — useful for retry logic and polling.

let retries = 0;
const maxRetries = 3;
let success = false;
 
while (retries < maxRetries && !success) {
  retries++;
  console.log(`Attempt ${retries}...`);
  // pretend the third attempt finally works
  if (retries === 3) success = true;
}
 
console.log(success ? "✅ Got through" : "❌ Gave up");

Output:

Attempt 1...
Attempt 2...
Attempt 3...
✅ Got through

The condition combines two checks: stop when we've used our retries OR when we've succeeded. Either one ends the loop.

How a for loop actually executes

Step 1 of 5

Initialise

Run the initialiser once: let i = 0. The counter is now ready.

The same five phases happen in while and for...of — they just hide the bookkeeping in different places.

break and continue

Two keywords interrupt the normal flow.

break exits the loop immediately. Useful when you've found what you needed, or hit a condition that should stop everything.

const results = ["pass", "pass", "fail-critical", "pass", "pass"];
 
for (const r of results) {
  if (r === "fail-critical") {
    console.log("Stopping suite — critical failure");
    break;
  }
  console.log(`✅ ${r}`);
}

Output:

✅ pass
✅ pass
Stopping suite — critical failure

continue skips the rest of this iteration and jumps to the next. Useful for "ignore this one, but keep going."

const results = ["pass", "pass", "fail", "pass", "fail"];
let failures = 0;
 
for (const r of results) {
  if (r === "pass") continue;   // not interested in passes
  failures++;
  console.log(`Failure #${failures}: ${r}`);
}
 
console.log(`Total failures: ${failures}`);

Output:

Failure #1: fail
Failure #2: fail
Total failures: 2

The infinite-loop trap

A loop without a working exit condition runs forever. The terminal hangs, the laptop fan spins up, you reach for Ctrl+C.

// ❌ Don't run this
let i = 0;
while (i < 10) {
  console.log(i);
  // forgot i++  — i stays 0 forever
}

Two habits avoid it:

  • After writing the loop body, immediately ask: "what changes the condition?" If you can't point at it, you have an infinite loop.
  • Avoid while (true) { ... } unless the body has an unconditional break you've already written. The pattern is occasionally legitimate (event loops, polling), but for everything else, prefer a real condition.

If you do hit an infinite loop in a Node.js script, Ctrl+C kills it. In a browser, close the tab.

Which loop to choose

A simple rule of thumb:

  • Iterating an array? Use for...of.
  • Need the index? Use a classic for with i as the counter.
  • Don't know how many iterations? Use while.
  • Iterating a fixed number of times? Either for works.

You'll write for...of more often than the other two combined.

⚠️ Common mistakes

  • Off-by-one errors in classic for loops. i <= testCases.length (note the <=) reads one past the end of the array — the last access is undefined. The convention i < array.length is correct.
  • Mutating the array you're iterating. for (let i = 0; i < arr.length; i++) { arr.splice(i, 1); } rewrites the array under the loop's feet — half the items get skipped. Either iterate a copy ([...arr]) or use array methods like filter (chapter 4).
  • Forgetting that break exits only the innermost loop. Nested loops can surprise beginners — break doesn't escape both. If you need that, factor the inner loop into a function and return from it.

🎯 Practice task

Build a small test runner. 20-25 minutes.

  1. In your js-for-qa folder, create runner.js.

  2. Declare an array of test results:

    const results = [
      { name: "login",    status: "pass" },
      { name: "checkout", status: "pass" },
      { name: "search",   status: "fail" },
      { name: "profile",  status: "skip" },
      { name: "logout",   status: "pass" }
    ];
  3. Use for...of to walk the array. For each result, print one of:

    • for "pass"
    • for "fail"
    • ⏭️ for "skip"
  4. After the loop, print a summary: total, passed, failed, skipped.

  5. Stretch 1 (continue): add a --quiet mode (a constant at the top: const quiet = true;). When quiet, use continue to skip printing pass and skip lines — only print failures.

  6. Stretch 2 (while): below your loop, write a while loop that simulates retrying a flaky request. Use Math.random() < 0.4 as "the request succeeded" and stop when either a success happens or you've tried 5 times. Print each attempt and the final result.

That ends Chapter 2. You can now write any synchronous decision-making code a test framework needs. The next chapter teaches functions — how to package the logic you've been writing into reusable, named pieces.

// tip to track lessons you complete and pick up where you left off across devices.