JavaScript While Loops

5 minute read

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 while when iteration count is unknown / condition-driven
  • ✅ Use do…while when 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
  • ✅ break early to exit on completion conditions
  • ❌ Don’t write a while that could be a clearer for
  • ❌ Don’t rely on coercion in the condition (while (i) to mean i !== 0)
  • ❌ Don’t await the 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.

← Previous: JS Loops (for) Next: JS Functions →

Updated: