SECTION 1
Historical Context & Motivation
From the earliest days of computing, programmers have struggled with a frustrating reality: loops are powerful, but they break in subtle ways. The two most infamous culprits are off-by-one errors (where your loop runs one time too many or one time too few) and infinite loops (where the loop never stops). These bugs have caused real-world software failures costing millions of dollars, and they remain among the most common mistakes programmers make at every skill level.
Understanding the history of these bugs helps us appreciate why careful loop design matters. Each milestone below represents a moment when the programming community learned—sometimes the hard way—that loop correctness is something you must deliberately verify, not assume.
1960s • Edsger Dijkstra & Structured Programming
1971 • The Fencepost Problem Named
1995 • JavaScript Is Born
2000s • Unit Testing Goes Mainstream
2010s–Now • Linters & Static Analysis
The central question this lesson addresses is: How can you write loops that do exactly what you intend—no more iterations, no fewer iterations, and no runaway execution? The answer lies in understanding boundary cases and developing a systematic habit of testing them.
SECTION 2
Core Principles & Definitions
Before you can fix loop bugs, you need to understand what they are and why they happen. Every loop has three critical parts: an initialization (where the counter or state starts), a condition (what keeps the loop running), and an update (how the counter or state changes each iteration). A bug in any one of these three parts can cause an off-by-one error or an infinite loop.
Off-By-One Error (OBOE)
< when you meant <=, or starting at 1 instead of 0. The result is a missing element or an array index out of bounds.Infinite Loop
Boundary Case
Loop Invariant
KEY TAKEAWAY
SECTION 3
Visual Explanation — Anatomy of a For Loop
The diagram below breaks down a standard JavaScript for loop into its three parts and traces the flow of execution. Pay close attention to when the condition is checked—it happens before each iteration, including the very first one. This means if the condition is false from the start, the loop body never runs at all.
Notice the feedback arrow from the update step back up to the condition. This is where infinite loops are born: if the update never moves i closer to violating the condition, you'll circle that path forever. And notice the condition uses < arr.length rather than <= arr.length. Changing that one character would cause an off-by-one error, trying to access an array index that doesn't exist.
SECTION 4
How Loop Bugs Happen — The Mechanics
Off-By-One Errors: < vs <=
In JavaScript, arrays are zero-indexed, meaning the first element is at index 0 and the last element is at index length − 1. A five-element array has valid indices 0, 1, 2, 3, and 4. If you write i <= arr.length, then on the last iteration i equals 5, and arr[5] is undefined. That's an off-by-one error.
VALID INDEX RANGE
The loop condition should be i < array.length (strict less-than) to stay within this range.
Iteration Count Formula
You can calculate exactly how many times a loop will run by using a simple formula. This is helpful for verifying your loop logic before you even run the code.
ITERATION COUNT (strict <)
For for (let i = start; i < end; i++), the loop runs end − start times. Example: start = 0, end = 5 → 5 iterations (i = 0, 1, 2, 3, 4).
ITERATION COUNT (inclusive <=)
For for (let i = start; i <= end; i++), the loop runs end − start + 1 times. This is one more than the strict version—the classic source of off-by-one errors.
Infinite Loops: The Update That Never Ends
An infinite loop occurs when the loop condition never becomes false. There are three common causes. First, the update is simply missing—you wrote while (i < 10) but never incremented i. Second, the update moves the variable in the wrong direction—you decrement when you should increment. Third, the condition is logically impossible to satisfy, such as checking while (x !== 10) when x increases by 3 from 1 and will skip right over 10 (1, 4, 7, 10—actually this one works, but x += 3 starting from 2 would give 2, 5, 8, 11, 14… and never hit 10).
Pro Tip
< or > over !== in loop conditions whenever possible. Relational operators like < are "self-correcting"—even if your variable jumps past the target, the loop still stops. Equality checks like !== can be skipped over entirely.SECTION 5
Boundary Case Testing — Your Safety Net
A boundary case (also called an edge case) is an input that sits right at the border of what your loop is designed to handle. Most loop bugs don't show up with "normal" inputs—they appear at the extremes. By systematically testing boundary cases, you can catch off-by-one errors and infinite loops before they cause problems in production.
Here is a concrete example. Suppose you write a function to sum all elements of an array. You test it with [1, 2, 3] and get 6—great! But have you tested it with []? If your loop starts with let sum = arr[0] and then loops from index 1, you'll get undefined for an empty array. Boundary case testing would have caught this immediately.
The Three-Test Rule
SECTION 6
Worked Example — Finding and Fixing a Buggy Loop
Let's walk through a realistic scenario. A student writes a function to find the largest number in an array but it has two bugs. We'll identify them using boundary case testing and fix them step by step.
Debug: findMax() with Off-By-One and Infinite Loop Risks
- 1
Step 1 — Read the Buggy Code
Here is the original function:function findMax(arr) { let max = arr[0]; let i = 0; while (i <= arr.length) { if (arr[i] > max) { max = arr[i]; } } return max; }At first glance it looks reasonable, but let's test the boundary cases. - 2
Step 2 — Test Boundary: Empty Array []
Whenarr = [],arr[0]isundefined, somaxstarts asundefined. Theni = 0andarr.length = 0, so the condition0 <= 0is true! The loop body runs, and sinceiis never updated, we have an infinite loop.Bug found: missingi++→ infinite loop - 3
Step 3 — Fix Bug #1: Add the Update
We addi++at the end of the while loop body. Now let's re-test the empty array. Witharr.length = 0and the conditioni <= arr.length, the first check is0 <= 0which is still true. The loop tries to comparearr[0](which isundefined) withmax. This shouldn't run at all for an empty array.Bug found:<=should be<→ off-by-one error - 4
Step 4 — Fix Bug #2: Correct the Condition
Changei <= arr.lengthtoi < arr.length. Also, we should startiat 1 since we already assignedarr[0]tomax. And we should handle the empty array case explicitly. - 5
Step 5 — Final Corrected Code
function findMax(arr) { if (arr.length === 0) return undefined; let max = arr[0]; for (let i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } return max; }Now let's verify:[]returnsundefined(correct).[7]→istarts at 1, condition1 < 1is false, returns 7 (correct).[3, 9, 1]→ loops through indices 1 and 2, returns 9 (correct).All three boundary tests pass ✓
SECTION 7
Common Loop Patterns — Strengths & Pitfalls
JavaScript offers several ways to write loops. Each has its own risk profile for off-by-one and infinite loop bugs. The table below compares the most common patterns and highlights where each one is most likely to trip you up.
| Loop Pattern | OBOE Risk | Infinite Loop Risk | Best Use Case |
|---|---|---|---|
for (let i = 0; i < n; i++) | Medium — watch < vs <= | Low — update is built into the syntax | Iterating over array indices |
while (condition) | Medium — depends on your manual setup | High — easy to forget update | When number of iterations is unknown |
for...of | Very Low — no index management | Very Low — iteration is automatic | When you need values, not indices |
.forEach() | Very Low — no index management | Very Low — iteration is automatic | Applying a function to each element |
do...while | Medium — runs at least once | High — same risks as while | When body must execute at least once |
Comparison of JavaScript loop constructs by bug risk
KEY TAKEAWAY
for loop, and always apply the three-test rule.SECTION 8
Connecting to Advanced Debugging & Testing
The boundary-case testing skills you're building now are the foundation of professional software testing. As you advance in computer science, you'll encounter formal methods for proving loop correctness and automated tools that generate test cases for you. The table below previews how today's concepts scale up.
| What You Learn Now | What Comes Next |
|---|---|
| Manually testing boundary cases (empty, single, normal) | Writing automated unit tests with frameworks like Jest or Mocha |
| Checking if a loop condition eventually becomes false | Formal loop invariants and termination proofs (taught in university CS) |
Using console.log() to trace loop variables | Using browser DevTools breakpoints and step-through debugging |
| Recognizing infinite loops when the page freezes | Static analysis tools (ESLint rules) that detect potential infinite loops before runtime |
Today's skills as a foundation for advanced debugging
In AP Computer Science and college-level courses, you'll learn about loop invariants—formal assertions that you can prove remain true at every step of a loop. For now, the practical skill of boundary-case testing gives you 90% of the benefit with much less theory. As your programs grow more complex—especially when loops are nested or when you use recursion—these debugging habits will save you hours of frustration.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
for (let i = 0; i <= arr.length; i++) is a bug when iterating over an array, but for (let i = 0; i < arr.length; i++) is correct. What would happen on the final iteration of the buggy version if arr = [10, 20, 30]?Show answer
[10, 20, 30] has length 3 and valid indices 0, 1, and 2. With <=, the loop runs when i = 3, and arr[3] is undefined because there is no element at that index. This is an off-by-one error. Using < makes the loop stop at i = 2, which is the last valid index.Question 2: BASIC CALCULATION
for (let i = 0; i < 5; i++) (b) for (let i = 1; i <= 5; i++) (c) for (let i = 3; i < 3; i++) Use the iteration count formulas from Section 4.Show answer
3 < 3 is false from the start, so the loop body never runs. This is the empty-input boundary case in action.Question 3: INTERMEDIATE
function reverseStr(str) {
let result = '';
for (let i = str.length; i >= 0; i--) {
result += str[i];
}
return result;
}Show answer
str.length is one past the last valid index. For "hi" (length 2), str[2] is undefined, so the result would start with "undefined". Testing with a single-character string like "a" reveals the problem: you'd get "undefineda". Fix: change the init to let i = str.length - 1. The corrected loop is for (let i = str.length - 1; i >= 0; i--).Question 4: APPLIED
while loop to advance through the questions array. A user reports that the app freezes. Here's the code:
function showQuestions(questions) {
let current = 0;
while (current < questions.length) {
displayQuestion(questions[current]);
let answer = getUserAnswer();
if (answer === 'skip') {
continue;
}
current++;
}
}
What causes the freeze, and how would you fix it?Show answer
continue statement jumps back to the top of the while loop without incrementing current. This creates an infinite loop: the same question is displayed over and over, and the user can never move forward. The fix is to move current++ before the continue, or place it at the top of the loop body so it always executes regardless of branching.Question 5: CRITICAL THINKING
let x = 1; while (x !== 100) { x += 7; } Will this loop terminate? Why or why not? Rewrite the condition so it's guaranteed to stop. Then generalize: when is it safe to use !== in a loop condition, and when should you use < or <= instead?Show answer
x !== 100 is always true, creating an infinite loop. Rewrite as while (x < 100). This terminates because x eventually exceeds 100 even if it never equals it. General rule: !== is only safe when you can guarantee the variable will hit the exact target value. Relational operators (<, <=) are safer because they catch values that overshoot the target.SUMMARY
Lesson Summary
Summary
The two most common loop bugs in JavaScript are off-by-one errors and infinite loops. Off-by-one errors happen when your loop runs one iteration too many or too few, usually because of confusion between < and <= or incorrect start values. Infinite loops occur when the update step is missing, moves in the wrong direction, or when the condition can never become false. Every loop has three critical parts—initialization, condition, and update—and a bug in any one of them can break your program.
The best defense is boundary case testing: always check your loop with an empty input, a single-element input, and a normal-sized input. Use the iteration count formula (end − start for <, end − start + 1 for <=) to verify your loop runs the expected number of times. When you don't need an index, prefer for...of or .forEach() to avoid index-management bugs entirely. Building these habits now will make you a more reliable programmer at every level.