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 unconditionalbreakyou'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
forwithias the counter. - Don't know how many iterations? Use
while. - Iterating a fixed number of times? Either
forworks.
You'll write for...of more often than the other two combined.
⚠️ Common mistakes
- Off-by-one errors in classic
forloops.i <= testCases.length(note the<=) reads one past the end of the array — the last access isundefined. The conventioni < array.lengthis 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 likefilter(chapter 4). - Forgetting that
breakexits only the innermost loop. Nested loops can surprise beginners —breakdoesn't escape both. If you need that, factor the inner loop into a function andreturnfrom it.
🎯 Practice task
Build a small test runner. 20-25 minutes.
-
In your
js-for-qafolder, createrunner.js. -
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" } ]; -
Use
for...ofto walk the array. For each result, print one of:✅for"pass"❌for"fail"⏭️for"skip"
-
After the loop, print a summary: total, passed, failed, skipped.
-
Stretch 1 (continue): add a
--quietmode (a constant at the top:const quiet = true;). When quiet, usecontinueto skip printing pass and skip lines — only print failures. -
Stretch 2 (while): below your loop, write a
whileloop that simulates retrying a flaky request. UseMath.random() < 0.4as "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.