Off-by-one errors and infinite loops
On This Lesson 11 topics
Lesson Overview
Two bugs account for most broken loops. The first is the off-by-one error: the loop runs one time too many or one time too few. The second is the infinite loop: the loop never reaches its finish line. Neither is a syntax error, so the compiler usually says nothing. The program runs, and gives wrong results or never returns.
On the track: an off-by-one error is running 4 laps when the race is 5, or 6 when it is 5. An infinite loop is forgetting to move the lap counter, so the finish line never arrives.
Why this matters: both bugs happen to experienced developers. What separates a beginner from a professional is a routine for catching them quickly, and this lesson gives you one. In a backend, an off-by-one error can skip the last record of a batch; an infinite loop can freeze a service.
This lesson builds on BEG-013-L01 to L05.
Learning Objectives
By the end of this lesson you will be able to:
- Recognise off-by-one errors, especially
<versus<=and counting from 0 versus 1. - Test a loop by hand at its first and last iteration.
- Compute how many times a loop runs from its bounds.
- Name the common causes of an infinite loop.
- Fix an infinite loop and stop a runaway program.
- Understand why changing the loop variable in the body is risky.
Written Explanation
Off-by-one errors
The name comes from being wrong by exactly one. There are three typical forms:
- Wrong comparison:
i < 5when you meanti <= 5, or the reverse. - Wrong start: counting from 0 when you meant 1.
- The fence-post problem: a fence with 5 sections needs 6 posts, not 5. Counting items and counting gaps between them differ by one.
A formula helps. For a loop with step 1 from a up to b:
| Condition | Runs |
|---|---|
i <= b | b - a + 1 |
i < b | b - a |
Check it: from 3 to 7 inclusive is 5 runs (7 - 3 + 1), not 4. The numbers 7 - 3 = 4 count the gaps, not the values.
The first-and-last test
For any loop, do this before running it:
- First iteration. What is the first value? Is that right?
- Last iteration. What is the last value for which the condition is still true? Is that the last value I want?
- One past the end. What is the first value that makes the condition false? Does the loop stop there?
These three questions catch nearly every off-by-one bug, and they cost ten seconds. It is the same habit as the boundary values you tested in BEG-012-L02.
Infinite loops
A loop is infinite when its condition never becomes false (and no break is reached). The usual causes:
- The update is missing, as in
while (i < 5) { print(i); }. - The update goes the wrong way:
for (int i = 10; i > 0; i++)counts up forever (until the value overflows, which BEG-009-L07 showed is not an escape you can rely on). - The wrong variable is updated: the condition reads
a, the body changesb. - A
continueskips the update in awhile, as in L04. - The condition can never be false by design, such as
while (true)without a reachablebreak.
Some infinite loops are written on purpose (a server waits for requests forever), but even those have a way to stop, such as a break or the server being shut down.
The compiler can see some of them
If the compiler can prove that a loop never ends, then code after it can never run, and Java reports an error:
while (true) {
System.out.println("tick");
}
System.out.println("done"); // unreachableThe compiler message (JDK 21) is:
L06b.java:6: error: unreachable statement
System.out.println("done");
^
1 errorHere the error is a hint: the line after the loop can never run, so there is probably a missing break. Most infinite loops, though, are not caught, because the condition uses variables whose values only the running program knows.
Stopping a runaway program
If your program is stuck, stop it yourself. In a terminal, press Ctrl+C. In an IDE the Run or Debug panel normally has a red Stop button, though its exact place depends on the IDE and its version. If a loop prints endlessly, stop it quickly, since the output can fill the screen or a log file.
Prevention: do not change the counter in the body
Changing the loop variable inside the body is a classic way to create both bugs:
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
i++; // second update: i skips every other value
}This prints 0 2 4 6 8. The loop is not wrong in Java's eyes, but the reader expects ten iterations, and gets five. If you want every second value, put i += 2 in the header.
A debugging routine
When a loop misbehaves:
- Print the counter and the accumulated values inside the body.
- Trace the first, the second, and the last iteration on paper.
- Check the condition at the boundary:
<or<=? - Check the update line: is it there, is it on the right variable, does it move toward the end?
- Only then use a debugger to step through (L07).
Examples
Example 1: < against <= (simple)
for (int i = 1; i < 5; i++) System.out.print(i + " ");
System.out.println();
for (int i = 1; i <= 5; i++) System.out.print(i + " ");
System.out.println();Output:
1 2 3 4
1 2 3 4 5 Only the operator differs. If the task says "print 1 to 5", the first version is off by one.
Example 2: counting inclusive (practical)
How many pages should a report loop visit if a user asks for pages 3 to 7?
int count = 0;
for (int i = 3; i <= 7; i++) count++;
System.out.println("3..7 inclusive runs " + count + ", 7-3=" + (7 - 3));Output:
3..7 inclusive runs 5, 7-3=4The loop visits 3, 4, 5, 6, 7: five pages. A developer who used 7 - 3 for the count would skip a page. This is the fence-post error in a real disguise.
Example 3: diagnosing a hanging loop (realistic)
A script should retry a connection up to 3 times, but hangs:
int attempt = 1;
while (attempt <= 3) {
System.out.println("Connecting, attempt " + attempt);
if (attempt == 2) {
continue; // "skip" the message on retry 2 ...
}
attempt++;
}Trace it: attempt 1 prints, then goes to 2. Attempt 2 prints, then continue runs, so attempt++ is skipped and attempt stays 2 forever: an infinite loop printing Connecting, attempt 2. The first-and-last test would have shown that the last iteration is never reached.
The fix is to move attempt++ before the continue (or to use a for). A well-behaved retry loop also checks for success and stops with break.
Backend Insight
- Pagination is full of off-by-one traps: page numbers that start at 1 against database offsets that start at 0. A wrong boundary shows duplicates or hides the last record of every page. Always test the first page, the last page, and an empty result.
- Batch jobs that loop over
nitems with< nor<= nare a frequent source of "the last record was never processed" tickets. - Runaway loops consume a CPU core entirely and, if they print or allocate, fill disks and memory. Production code puts a limit on every loop that depends on outside conditions, such as a maximum number of retries.
Common Mistakes
Mistake 1: Trusting the count in your head
Run the three questions (first, last, one past the end) instead.
Mistake 2: Updating the wrong variable
- Incorrect:
while (a < 10) { b++; } - Correct: update the variable the condition reads.
Mistake 3: Counting down with the wrong operator
for (int i = 10; i > 0; i++) should be i--. The loop condition and the update must agree on direction.
Mistake 4: Fixing a hang by guessing
Add a print of the counter, run, and stop with the Stop button. The output shows what changes and what does not.
Mistake 5: Adding an artificial limit to hide the bug
A limit such as "stop after 1,000 iterations" prevents damage, but it does not explain why the loop did not end. Find the cause first, then keep the limit as a safeguard.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. How many times does for (int i = 5; i <= 12; i++) run?
Show solution A1
A1. 8 times (5, 6, 7, 8, 9, 10, 11, 12).
A2. The task is to print 1 to 10. Which header is correct: i < 10 or i <= 10?
Show solution A2
A2. i <= 10 (with i starting at 1). With < 10 it stops at 9.
A3. Name two causes of an infinite loop.
Show solution A3
A3. For example: the update is missing; the update moves the wrong way; the wrong variable is updated; continue skips the update.
Level B: Application
B1. The loop should print the numbers 10 down to 1 but prints nothing. Find the bug and fix it.
for (int i = 10; i < 1; i--) {
System.out.println(i);
}Show solution B1
B1. Counting down needs >=, and the test was wrong: 10 < 1 is false at once. Fix: for (int i = 10; i >= 1; i--).
B2. The code below should print 0 to 4, but hangs. Fix it in two different ways.
int i = 0;
while (i < 5) {
System.out.println(i);
}Show solution B2
B2. The update is missing. Fix 1: add i++; in the body. Fix 2: use for (int i = 0; i < 5; i++).
B3. A report must include months 4 to 9 inclusive. Write the loop and say how many months it visits.
Show solution B3
B3.
for (int month = 4; month <= 9; month++) {
System.out.println("Month " + month);
}It visits 6 months (9 - 4 + 1).
Level C: Challenge
C1. Apply the first-and-last test to for (int n = 2; n < 20; n += 3). List the first value, the last value that runs, and the first value that stops it.
Show solution C1
C1. First value 2. The values are 2, 5, 8, 11, 14, 17, so the last that runs is 17. The next value is 20, which fails n < 20, so the loop stops there.
C2. A teammate says: "My loop while (x != 10) with x += 3 starting at 0 never ends." Explain why, and propose a better condition.
Show solution C2
C2. x goes 0, 3, 6, 9, 12, ..., so it jumps over 10 and x != 10 is always true. A safer condition is x < 10, which ends as soon as x reaches or passes the limit. This "not equal" pattern is a common cause of infinite loops when steps are not 1.
Mini Challenge
Take this loop that should print the even numbers from 2 to 20:
for (int i = 2; i < 20; i += 2) {
System.out.print(i + " ");
}- Apply the first-and-last test and find the bug.
- Fix it, run it, and write down what it prints.
- Write a second version using a
whileloop. Check that the update is inside the body and cannot be skipped.
Check your work: the output has 10 numbers and ends with 20, and both versions print the same line.
Summary & Next Lesson
Key takeaways
- An off-by-one error is a loop that runs one time too many or too few; check
<versus<=and the start value. - Inclusive count from
atobisb - a + 1. - Test the first iteration, the last iteration, and the first value that stops the loop.
- An infinite loop never makes its condition false: missing update, wrong direction, wrong variable, or a skipped update.
- Prefer
<or>conditions to!=when the step is not 1, and put a safety limit on loops that depend on the outside world.
Next lesson
The next lesson is "Demo: number guessing game, stepping through the loop in the debugger". You will build the guessing game and use a debugger to watch the loop variable change.
Self-check
- How many runs does a loop from 3 to 7 inclusive have?
- Name the three questions of the first-and-last test.
- What is one cause of an infinite
whileloop? - Why is
x != 10risky whenxgoes up by 3? - How can you stop a runaway program?
Show answers
- Five.
- First value, last value that runs, first value that stops the loop.
- The variable in the condition is never updated (other causes are also accepted).
xcan jump over 10, so the condition stays true forever.- Press
Ctrl+Cin a terminal, or use the Stop button in the IDE (the exact place varies).
References
- Oracle, Java Language Specification, SE 21, Chapter 14. It defines
whileandforevaluation order, which the first-and-last test relies on. The unreachable-statement error shown above was produced by compiling the example with JDK 21; the unreachable-code rule itself comes from the language specification's reachability rules, which were not re-read on the page. - Oracle, The while and do-while Statements. It supports that
while (true)is a valid infinite loop. - Examples 1 and 2 and the
i++skipping loop were compiled and run with JDK 21. Example 3 was traced by hand and not run, to avoid starting an infinite loop. The location of the Stop button depends on the IDE and was not verified.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.