TL;DR: A loop repeats code automatically. JavaScript has four main loop types: for (classic, index-based), while (runs until condition is false), for...of (modern, iterates any iterable), and forEach (array method). For async work inside loops, always use for...of — not forEach.
Why AI Coders Need to Know This
Loops are everywhere in backend code — processing database records, sending emails to a list, scraping multiple pages, running migrations. When you ask AI to "send a welcome email to all users" or "update all product prices," it writes a loop. Understanding the type of loop, how to control it, and the async/await pitfall makes the difference between code that works and code that silently skips half your data.
Real Scenario
Prompt I Would Type
Write a Node.js script that:
1. Fetches all users from the database
2. Sends each user a personalized email via the SendGrid API
3. Logs success or failure for each user
4. Waits between emails to avoid rate limiting (100ms delay)
5. Continues even if one email fails
What AI Generated
import { db } from './lib/db.js';
import { sendEmail } from './lib/email.js';
async function sendWelcomeEmails() {
const users = await db.user.findMany({ where: { welcomeEmailSent: false } });
console.log(`Processing ${users.length} users...`);
let success = 0, failed = 0;
// for...of — the correct choice for async loops
// Unlike forEach, for...of respects await — each iteration waits to finish
for (const user of users) {
try {
await sendEmail({
to: user.email,
subject: `Welcome, ${user.name}!`,
body: `Hi ${user.name}, thanks for joining...`
});
// Mark as sent in the database
await db.user.update({
where: { id: user.id },
data: { welcomeEmailSent: true }
});
success++;
console.log(`✓ Sent to ${user.email}`);
// Rate limit: wait 100ms between emails
await new Promise(resolve => setTimeout(resolve, 100));
} catch (err) {
// continue to next user even if this one fails
failed++;
console.error(`✗ Failed for ${user.email}:`, err.message);
}
}
console.log(`Done. Success: ${success}, Failed: ${failed}`);
}
sendWelcomeEmails();
Understanding Each Part
The Classic for Loop
// Structure: for (initialization; condition; increment)
for (let i = 0; i < 5; i++) {
console.log(i); // 0, 1, 2, 3, 4
}
// i = 0 to start
// i < 5 = keep going while true
// i++ = add 1 after each iteration
// Count down
for (let i = 10; i >= 0; i--) { console.log(i); } // 10, 9, 8...0
// Skip even numbers
for (let i = 0; i < 10; i += 2) { console.log(i); } // 0, 2, 4, 6, 8
// Iterate an array by index (when you need the index)
const fruits = ['apple', 'banana', 'cherry'];
for (let i = 0; i < fruits.length; i++) {
console.log(`${i}: ${fruits[i]}`); // "0: apple", "1: banana", "2: cherry"
}
for...of — Modern Array Iteration
// Cleaner than classic for — no index tracking needed
const fruits = ['apple', 'banana', 'cherry'];
for (const fruit of fruits) {
console.log(fruit); // apple, banana, cherry
}
// Works on strings too
for (const char of 'hello') {
console.log(char); // h, e, l, l, o
}
// Works on Maps and Sets
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
console.log(key, value); // a 1, b 2
}
// Need the index? Use entries()
for (const [index, fruit] of fruits.entries()) {
console.log(`${index}: ${fruit}`);
}
while — Loop Until a Condition Is False
// Runs as long as condition is true
let attempts = 0;
let success = false;
while (!success && attempts < 3) {
try {
await connectToDatabase();
success = true; // exits the loop
} catch {
attempts++;
console.log(`Attempt ${attempts} failed, retrying...`);
await sleep(1000); // wait 1 second before retry
}
}
// do...while: always runs at least once
let input;
do {
input = prompt('Enter a number greater than 0:');
} while (Number(input) <= 0);
forEach — Array Method (Not Async-Safe)
const numbers = [1, 2, 3, 4, 5];
// forEach calls a function for each item
numbers.forEach(num => {
console.log(num * 2); // 2, 4, 6, 8, 10
});
// ❌ WRONG: await inside forEach doesn't work as expected
numbers.forEach(async num => {
await processNumber(num); // all fired simultaneously, not sequentially
});
// ✅ CORRECT: use for...of for async work
for (const num of numbers) {
await processNumber(num); // each waits before the next starts
}
break and continue — Loop Control
// break: exit the loop entirely
for (const user of users) {
if (user.role === 'admin') {
console.log('Found admin:', user.name);
break; // stop searching once found
}
}
// continue: skip this iteration, go to the next
for (const user of users) {
if (!user.email) {
console.log(`Skipping ${user.name} — no email`);
continue; // skip to next user
}
await sendEmail(user.email);
}
Parallel vs Sequential Async Loops
const userIds = [1, 2, 3, 4, 5];
// Sequential: one at a time (safe for rate limits)
for (const id of userIds) {
await fetchUser(id); // waits for each before starting next
}
// Parallel: all at once (fast, but can overwhelm APIs)
const users = await Promise.all(userIds.map(id => fetchUser(id)));
// All 5 requests fire simultaneously
// Controlled parallel: batches of 3
for (let i = 0; i < userIds.length; i += 3) {
const batch = userIds.slice(i, i + 3);
await Promise.all(batch.map(id => fetchUser(id)));
await sleep(500); // pause between batches
}
What AI Gets Wrong About Loops
1. Using forEach with Async/Await
The most common loop mistake in AI-generated code. array.forEach(async item => { await ... }) fires all iterations simultaneously and doesn't return a promise — the outer function can complete before any of the async operations finish. Use for...of for sequential async work, or Promise.all(array.map(...)) for parallel.
2. Forgetting to Continue After Errors
In batch processing loops, AI sometimes puts the entire loop body in a try/catch without continue — meaning a single failure stops processing all remaining items. Wrap each item's processing in its own try/catch and log the error, then let the loop proceed to the next item.
3. Infinite Loops via Missing Increments
While rare in AI code, while (true) loops without a guaranteed exit condition can lock up your process. Always ensure while loops have a reachable exit condition, and consider adding a safety counter: if iterations exceed an expected maximum, break and log a warning.
4. N+1 Database Queries Inside Loops
Fetching a list, then querying the database again for each item inside a loop, creates an N+1 query problem — 1 query for the list plus N queries for the details. AI frequently generates this pattern. Solve it by loading all related data in a single query with JOINs or Prisma's include option before the loop.
How to Debug Loops with AI
When a loop isn't processing items as expected, add a counter log: console.log(`Processing item ${i+1} of ${items.length}:`, item). This immediately shows you if the loop is running at all, which items it's hitting, and where it stops. Paste the log output and loop code into Cursor or Claude for diagnosis.
For async loop issues, ask: "Is this loop sequential or parallel? Will each iteration wait for the previous to complete?" Claude is very good at explaining async execution order from code.
What to Learn Next
Frequently Asked Questions
A loop is a control structure that repeats a block of code multiple times. Instead of writing the same code ten times, you write it once and tell the program to run it ten times (or until a condition is met). Loops are how programs process lists, build strings, wait for events, and automate repetitive tasks.
for: the classic loop with index control — best when you need the index or non-standard iteration. for...of: cleaner syntax for iterating over any iterable (arrays, strings, Maps) — best for modern JavaScript. forEach: an array method that calls a function on each item — cannot be broken out of early, cannot use await inside without workarounds.
Use for when you know in advance how many times to loop (iterating an array, counting to N). Use while when you loop until a condition changes and you don't know how many iterations you'll need — polling for a result, reading user input, waiting for a file to process.
forEach doesn't support async/await properly — it fires all iterations simultaneously without waiting for each to finish. If you need to await something inside a loop, use for...of instead: for (const item of items) { await processItem(item) }. This processes items one at a time in sequence.
An infinite loop runs forever because the exit condition is never met — usually caused by a while condition that never becomes false, or a for loop with a bad increment. Signs: browser tab freezes, Node.js process maxes out CPU. Fix: add a missing increment (i++), correct the condition, or add a safety break counter.