Off-by-One Errors: The Bug Every Coder Hits

By Mark Sullivan2026-04-15~7 min read

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

  1. For an array of length n, valid indexes are 0..n-1. Use i < n, never i <= n.
  2. The last item is at n-1, not n.
  3. 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] is undefined, not an error. Easy to miss.
  • Negative indexes do not work directly: arr[-1] is undefined. Use arr.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 silent undefined, which is actually safer.

Java:

  • arr.length is 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.


T
Tom Reyes
Reviewer · 12 yrs Java/JVM

Tom spent eight years as a backend engineer in fintech (Java + Kotlin) and four as a lead at an enterprise SaaS company. He reviews every Java example on this site and writes the data-structure deep dives. He cares deeply that beginners aren't taught bad habits they'll have to unlearn.

Why Programmers Count From Zero

This is the question every new programmer asks. The short answer: zero-based numbering makes pointer arithmetic and offset calculations cleaner at the machine level. Edsger Dijkstra's 1982 memo argued the case formally; most languages designed since have followed it.

The practical reason this matters in 2026: even when you are not doing pointer arithmetic, the language you are using was designed by people who were. The convention is baked in. Fighting it is more expensive than learning it.

Three More Off-by-One Patterns Worth Memorizing

  1. Loops that compute between two indexes. Iterating with i <= end when you meant i < end is one of the most common patterns.
  2. Slicing strings. "hello".slice(0, 5) returns "hello" - the end index is one past the last character.
  3. Date ranges. Find all events from 2026-01-01 to 2026-01-31 - inclusive of the 31st? Read the docs.