What Is an Off-by-One Error? The Bug Every Loop Hides

- Off-by-one error is the fencepost problem: confusing the count of items with the count of boundaries between them
- Half-open intervals
[lo, hi)are the standard fix — include the start, exclude the end, use<not<=against lengths for i in range(n)runs exactlyntimes;range(n+1)runsn+1times and crashes on ann-length array- Binary search midpoint overflow:
(lo + hi) / 2wraps negative for large arrays; uselo + (hi - lo) / 2instead - Mixing conventions — closed interval in one place, half-open in another — is where most binary search off-by-one bugs live
- Catch boundary bugs by manually tracing
n = 0,n = 1, and the last element before submitting
You have a 30-foot fence. Posts are spaced 3 feet apart. How many posts do you need?
Most people say 10. Confidently. Wrong.
The answer is 11, and you just committed an off-by-one error while reading a blog post about off-by-one errors. That gap between 10 and 11 has a proper name, a 50-year history, and a standing appointment with your binary search the morning of an interview.
The Fencepost Problem
The confusion is called the fencepost error (sometimes "picket fence problem"). There are two different things to count: the sections of fence, and the posts that separate them. A 30-foot fence with posts every 3 feet has 10 sections and 11 posts. Counting sections is intuitive. Counting posts is where everyone quietly miscounts and then acts like they totally knew.
Off-by-one errors happen when you count things separated by boundaries, and you confuse whether the boundaries themselves get counted.
In code, this shows up every time you write a loop, slice an array, or mark a range. The boundaries are the index values. Include or exclude them incorrectly and your result shifts by exactly one.
Where Loops Go Wrong
Take the simplest possible loop:
for i in range(10): print(i)
This prints 0 through 9. Ten numbers. The range is [0, 10), meaning 0 is included and 10 is excluded. That's the half-open interval convention most languages use, and it exists specifically to prevent this confusion.
Now suppose you want to iterate over an array. Array indices in most languages go from 0 to n-1, where n is the length.
items = [10, 20, 30, 40, 50] # length 5, indices 0..4 # Correct for i in range(len(items)): print(items[i]) # Off by one: tries to access items[5], which doesn't exist for i in range(len(items) + 1): print(items[i])
The second loop crashes. You accessed index 5 on an array that only goes to 4. One extra iteration, one index out of bounds.
The opposite mistake is quieter and nastier:
# Off by one: never processes the last element for i in range(len(items) - 1): print(items[i])
This one doesn't crash. It silently skips the last element, and your output is wrong without any error message to tell you why.
The crash is actually a gift. The silent wrong answer is the one that costs you an afternoon of staring at code you're convinced is correct.
The < vs <= Trap
The most frequent off-by-one decision in loop writing is choosing between < and <=. One character. Your entire result depends on it.
n = 5 total = 0 # Correct: indices 0, 1, 2, 3, 4 i = 0 while i < n: # <, not <= total += items[i] i += 1
Write while i <= n and you get an extra iteration where i = 5, and items[5] does not exist.
The rule that makes this consistent: use half-open intervals by default. Start at 0, stop before n. Use <, not <=, against lengths.

The boundary check is almost right. Almost.
Slices and Ranges
Python makes the convention visible:
s = "hello" print(s[1:4]) # "ell", index 1 included, index 4 excluded print(s[1:5]) # "ello", up to but not including 5
The length of s[a:b] is always b - a. That arithmetic only works cleanly with half-open intervals. Closed intervals require b - a + 1, which adds a fencepost-style extra step every time you compute a length.
![Diagram showing closed interval [lo, hi] vs half-open interval lo, hi) on a number line, illustrating that half-open length is hi minus lo while closed is hi minus lo plus one
Java and some other languages use closed-range conventions in certain libraries. Always check which convention a library uses before calling it. Getting it wrong silently returns one element too few or too many, and you will spend 45 minutes convinced the library is broken before realizing it was you the whole time.
The Most Famous Off-by-One Bug in Computing
In June 2006, Joshua Bloch published a post titled "Extra, Extra, Read All About It: Nearly All Binary Searches and Mergesorts Are Broken." The bug was this:
int mid = (low + high) / 2;
Looks correct. Mathematically, it is correct. For 32-bit integers, it is not.
int mid = low + (high - low) / 2; // or in Java: int mid = (low + high) >>> 1; // unsigned right shift, no overflow
When low and high are both large positive integers, their sum overflows a 32-bit signed integer and wraps to a negative number. A negative divided by 2 is still negative. Negative index. Crash.
This bug lived in Java's Arrays.binarySearch for roughly nine years before anyone noticed. It only triggers when the array holds more than about one billion elements. Most tests never get anywhere near that scale. The code looked correct to every reviewer, because the formula is mathematically right for infinite integers. It is only wrong for bounded 32-bit ones.

The test suite never covered edge cases from three thousand years ago.
This is why the midpoint formula comes up in interviews. Not a trick question. A real bug that shipped in production, in widely-reviewed code, for a decade.
Off-by-One in Binary Search Bounds
Past the overflow, binary search has a second off-by-one trap: the loop termination condition and how you update the bounds.
def binary_search(nums, target): lo, hi = 0, len(nums) - 1 while lo <= hi: # <= because both bounds are inclusive mid = lo + (hi - lo) // 2 if nums[mid] == target: return mid elif nums[mid] < target: lo = mid + 1 # mid is already checked, so +1 else: hi = mid - 1 # mid is already checked, so -1 return -1
The while lo <= hi uses <= because both lo and hi are valid indices to check. Write while lo < hi and you exit one iteration early, missing the answer when it sits exactly at lo == hi.
Alternatively, with a half-open interval [lo, hi):
lo, hi = 0, len(nums) # hi is one past the end while lo < hi: # < because hi is exclusive mid = lo + (hi - lo) // 2 if nums[mid] < target: lo = mid + 1 else: hi = mid # not mid-1, because hi is already exclusive
Both are correct. They use different conventions and each requires its own consistent < or <= choice. Mixing conventions inside the same function is exactly where the off-by-one sneaks in. Pick one style before you start, write it at the top of the function, and commit.
Why Interviews Care
Off-by-one errors are one of the most common reasons interviewers see working logic produce a wrong answer. The algorithm is right. The bounds are off. Every edge case fails.
Places interviewers watch for them:
Array traversal. Did you handle the last element? Did you accidentally include index n?
Sliding window. Window start and end, inclusive or exclusive? When you advance, are you consistent?
Prefix sums. A common trap: prefix[i] often represents the sum of nums[0..i-1], not nums[0..i]. Pick a convention and hold it everywhere.
# prefix[i] = sum of first i elements prefix = [0] * (n + 1) for i in range(n): prefix[i + 1] = prefix[i] + nums[i] # Sum of nums[l..r] inclusive: range_sum = prefix[r + 1] - prefix[l]
Drop the +1 on either side of that subtraction and you're off by one element.
String problems. Substring boundaries, palindrome checks, two-pointer convergence.
Off-by-one bugs are almost always caught by testing your code on a small example. Walk through your loop manually for n = 1, n = 2, and an empty input. Three cases, five minutes, and you find the boundary mistake before the interviewer does.
Two Tests That Catch the Bug
Test 1: Walk the boundaries. Run your loop logic by hand for the first and last iteration. Does the first iteration touch index 0? Does the last touch index n-1? Or did you quietly skip one?
Test 2: Count the iterations. A loop from i = 0 to i < n runs exactly n times. A loop from i = 0 to i <= n runs n + 1 times. A loop from i = 1 to i < n runs n - 1 times. Count these explicitly on paper when the bounds feel uncertain.
If you want a deeper look at the four structural sources of off-by-one errors in interview problems, Off-By-One Errors Come From Four Places. Stop Making Them. breaks each one down.
Catching a boundary bug in a voice interview means narrating your manual trace out loud. SpaceComplexity runs realistic mock interviews where that exact pressure is part of the drill.
What to Remember at the Boundary
- An off-by-one error is the fencepost problem: confusing the count of items with the count of separators between them
- Loops from
0to< niterate exactlyntimes. Loops from0to<= niteraten + 1times. One of those crashes on ann-length array. - Use half-open intervals
[lo, hi)by default: include the start, exclude the end - The binary search midpoint
(lo + hi) / 2overflows for large inputs; uselo + (hi - lo) / 2 - Mixing loop bound conventions (
<vs<=) inside a single function is where most binary search bugs live - Test boundary cases manually:
n = 0,n = 1, and the last element of any array