Off-by-One Errors: The Bug Every Coder Hits
Twelve years into a programming career, I still pause before writing i < xs.length. A mental model that mostly fixes off-by-one errors.
Why This Bug Never Goes Away
Programmers count from 0. The world counts from 1. Every time you index into anything, your brain has to do a small translation. The bug is not carelessness — it is the translation occasionally going wrong under any kind of cognitive load.
This is not a beginner-only problem. I have seen senior engineers ship off-by-one bugs in production at every company I have worked for. Including myself. Including yesterday. The bug never fully goes away because the mental translation never becomes automatic. It becomes faster, but it stays a translation.
The most you can do is build habits and tests that catch the translation when it goes wrong. That is what this post is about.
Three Rules That Catch Most of Them
- For an array of length n, valid indexes are
0..n-1. Usei < n, neveri <= n. - The last item is at
n-1, notn. - When in doubt, use a name-based loop (
for-of,for-each) so you never touch the index.
Rule three is the cheat code. If you can avoid touching the index entirely, you cannot make an off-by-one error on that loop. Modern languages all have a syntax for index-free iteration. Use it whenever the problem does not specifically require an index.
// Safer: never touch i
for (const x of xs) { use(x); }
// Risky: easy to off-by-one
for (let i = 0; i < xs.length; i++) { use(xs[i]); }Half-Open Intervals
Most APIs use half-open intervals: [start, end) — start is included, end is not. arr.slice(0, 3) returns indexes 0, 1, 2. range(0, 5) returns 0, 1, 2, 3, 4. Memorize this. It is the dominant convention.
The reason half-open won out historically is that it makes adjacent slices easy to express: arr.slice(0, 3) and arr.slice(3, 6) together cover indexes 0 through 5 without overlap. If both endpoints were inclusive, you would have to subtract one in the middle, and the off-by-one bugs would be even more common.
The mental model that helps: imagine the indexes as fence posts between items, not the items themselves. slice(0, 3) means "between fence post 0 and fence post 3" — which captures items 0, 1, 2.
Test the Edges
For any code with bounds, write tests for: empty input, single element, exactly the boundary value. 80 percent of off-by-one bugs surface immediately when you test the empty array.
I have a habit at this point: when I write a function that takes an array, I immediately write three test cases: [], [1], and [1, 2, 3]. The empty case catches the "do not enter the loop at all" path. The single-element case catches anything that assumed there is a "next" item. The three-element case catches the typical happy path.
If your code passes all three, the off-by-one error is unlikely. If it fails one of them, you have just found your bug — and probably a whole class of related bugs you would have shipped without those tests.
Language-Specific Gotchas
JavaScript:
arr[arr.length]isundefined, not an error. Easy to miss.- Negative indexes do not work directly:
arr[-1]isundefined. Usearr.at(-1)in modern JS.
Python:
- Negative indexes do work:
arr[-1]is the last element. Convenient, but confusing if you came from JavaScript. - Out-of-range indexes raise
IndexError— louder than JavaScript's silentundefined, which is actually safer.
Java:
arr.lengthis a property, not a method.arr.length()is a syntax error. Even seniors mix this up after switching languages.- Out-of-range raises
ArrayIndexOutOfBoundsException. The error name is verbose, the meaning is the same.
Watch the Bugs in Real Code
Our common loop bugs page shows the off-by-one bug in action with the fix. Five real broken snippets, each with the explanation of what happened and the corrected version.
If you want to harden your spotting muscle, try our Spot the Bug mini-game. About two of the rotating puzzles are off-by-one variants. Get them all right and you have moved this bug from "will hit me" to "can recognize on sight" — which is the realistic goal.
The Takeaway
You will hit off-by-one errors for the rest of your career. The goal is not to never hit them — it is to recognize them within seconds when you do. The combination of zero-indexed loops, half-open intervals, and edge-case tests is the toolkit. None of them eliminate the bug entirely, but together they shrink the time-to-fix from hours to seconds.
If you are a beginner reading this and wondering whether this is too much detail: it is not. Off-by-one errors are the single most common bug in beginner code I review. Investing 20 minutes in this article saves you a weekend of confusion across the next three months.