JavaScript While Loops
while and do…while are the loops you reach for when you don’t know the iteration count up front. Polling, retry-with-backoff, parsing until a delimiter, draining a queue — anything condition-driven is a while job. They’re simpler than for, but the lack of a built-in counter makes infinite loops a real risk.
while — Test First
let n = 5;
while (n > 0) {
console.log(n);
n--;
}
// Output:
// 5
// 4
// 3
// 2
// 1
Flow:
┌──────────────┐
│ test cond │◄──┐
└──────┬───────┘ │
true │ false │
▼ │
┌──────────────┐ │
│ body │───┘
└──────────────┘
If the condition is false at the start, the body never runs.
do…while — Test After
let n = 5;
do {
console.log(n);
n--;
} while (n > 0);
The body runs at least once, then the condition is checked. Use this when the first iteration is unconditional — most commonly, prompting the user, then asking again if input was invalid.
while |
do…while |
|
|---|---|---|
| Test runs | before body | after body |
| Min iterations | 0 | 1 |
| Use when | Condition might be false on entry | First pass is unconditional |
Polling and Waiting
async function waitForElement(selector, timeoutMs = 5000) {
const start = Date.now();
let el = document.querySelector(selector);
while (!el) {
if (Date.now() - start > timeoutMs) {
throw new Error(`Timed out waiting for ${selector}`);
}
await new Promise(r => setTimeout(r, 100)); // wait 100ms
el = document.querySelector(selector);
}
return el;
}
The loop runs an unknown number of times — a for would be awkward here.
Retry With Backoff
async function fetchWithRetry(url, maxAttempts = 5) {
let attempt = 0;
let delay = 200;
while (attempt < maxAttempts) {
try {
const res = await fetch(url);
if (res.ok) return await res.json();
throw new Error(`HTTP ${res.status}`);
} catch (err) {
attempt++;
if (attempt >= maxAttempts) throw err;
console.log(`Retry ${attempt} after ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
delay *= 2; // exponential backoff
}
}
}
while lets you express “keep trying until success or maxAttempts” without a fixed iteration count.
Parsing Until a Delimiter
function takeUntil(input, delim) {
let i = 0;
while (i < input.length && input[i] !== delim) {
i++;
}
return { token: input.slice(0, i), rest: input.slice(i + 1) };
}
takeUntil("hello,world,!", ",");
// { token: "hello", rest: "world,!" }
Walking a string with a moving cursor — the bread and butter of small parsers.
Draining a Queue
const queue = [1, 2, 3, 4, 5];
while (queue.length > 0) {
const item = queue.shift();
console.log("processing", item);
}
console.log(queue); // []
The condition naturally tracks “more work to do?” — no manual index needed.
break and continue
Same semantics as in for:
let n = 0;
while (true) { // intentionally infinite
n++;
if (n === 7) continue; // skip 7
if (n > 10) break; // stop after 10
console.log(n);
}
// Output: 1 2 3 4 5 6 8 9 10
while (true) { ... break; } is a legitimate pattern when the exit condition is too complex to fit into the header. ESLint’s no-constant-condition rule has an option to allow it inside loops.
Infinite Loop Pitfalls
// ❌ Forgot to update the variable
let i = 0;
while (i < 5) {
console.log(i);
// i never increments → infinite loop
}
// ❌ Type confusion: string never matches number
let i = "0";
while (i < 5) { // "0" < 5 → true (coerced)
console.log(i);
i = i + 1; // string concat: "01", "011", ...
} // never numerically becomes 5
// ✅ Always update the loop variable, mind the types
let i = 0;
while (i < 5) {
console.log(i);
i++;
}
Warning: Browsers will eventually freeze on a tight infinite loop. In the dev console you can break with the Pause button or close the tab. In Node, Ctrl+C kills the process.
Safety Counter for Bounded Loops
Even when a loop should terminate, defensively cap it:
let safety = 0;
while (queue.length > 0) {
if (++safety > 10_000) {
throw new Error("Loop guard tripped");
}
process(queue.shift());
}
Especially useful when iterating over data that comes from outside (API responses, user input). A bug in the data shouldn’t be able to hang your tab.
A Realistic Example — Reading a Stream
async function readJSONStream(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
const items = [];
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// newline-delimited JSON
let newlineIdx;
while ((newlineIdx = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, newlineIdx);
buffer = buffer.slice(newlineIdx + 1);
if (line.trim()) items.push(JSON.parse(line));
}
}
return items;
}
Two whiles nested — the outer drains the network stream, the inner drains complete lines from the buffer. Both use break/continue rather than fighting a counter.
Choosing Between for, while, and do…while
Do you know iteration count up front?
│
├── Yes → for (let i = 0; i < N; i++)
│
└── No → Is your iterable a known collection?
│
├── Yes → for…of
│
└── No → Should body run at least once?
│
├── Yes → do…while
│
└── No → while
Common Mistakes
// ❌ Off-by-one
let i = 0;
while (i <= arr.length) { // <= goes one past end
console.log(arr[i]); // last is undefined
i++;
}
// ❌ Modifying collection without adjusting condition
while (i < arr.length) {
if (arr[i] < 0) arr.splice(i, 1);
i++; // skips the next element after a removal
}
// ❌ Async with await but the condition never changes
let done = false;
while (!done) {
await fetch(...);
// forgot to set done = true
}
// ✅ Update the loop state every iteration; no exceptions
Best Practices Checklist
- ✅ Use
whilewhen iteration count is unknown / condition-driven - ✅ Use
do…whilewhen the first pass is unconditional - ✅ Always update the loop state inside the body — every path
- ✅ Add a safety counter for loops over external/uncontrolled data
- ✅
breakearly to exit on completion conditions - ❌ Don’t write a
whilethat could be a clearerfor - ❌ Don’t rely on coercion in the condition (
while (i)to meani !== 0) - ❌ Don’t
awaitthe same call without changing the condition
What’s Next?
You can express any control flow now. Time for the most-reused unit of code — functions. Declarations, expressions, parameters, return values, and the rules around them.