LeetCopilot Logo
LeetCopilot
Home/Blog/Why Do I Keep Getting Off-By-One Errors on LeetCode (And How to Fix Them)

Why Do I Keep Getting Off-By-One Errors on LeetCode (And How to Fix Them)

Marcus Kim
Dec 4, 2025
14 min read
Beginner guideDebuggingCommon MistakesArraysLoopsProblem solving
Off-by-one errors plague beginners because our brains count differently than computers index. Learn why these bugs happen, how to spot them before running code, and the systematic checks that prevent them.

Your code passes 47 out of 48 test cases. The last one fails: IndexError: list index out of range.

You stare at the loop. It looks correct. You add a print statement. The index goes to n when it should stop at n - 1.

You fix it: change i <= n to i < n. Submit. It passes.

Two problems later, the same error. Different loop, same mistake.

This is the Off-By-One Curse: You understand what off-by-one errors (OBOEs) are, but you keep making them anyway. Your brain sees array.length and thinks "count to here," but the computer sees "this exceeds valid indices."

This guide explains why beginners can't stop making OBOEs and teaches you the systematic checks that catch them before you hit "Run."

TL;DR

  • Root cause: Human brains count from 1 ("first, second, third"). Computers index from 0. Your intuition fights against zero-based indexing constantly.
  • Most common: Loop conditions (<= vs <), string slicing ranges ([:n] vs [: n + 1]), and "fence-post" counting (10 meters needs 11 posts, not 10).
  • Prevention system: Pre-execution mental checklist: "Last valid index?", "Loop runs how many times?", "What if array is empty?"
  • Debugging technique: Trace with boundary values (empty array, single element, exact limit) before normal cases.
  • Common mistake: Fixing the symptom (change <= to <) without understanding why, so you repeat the error in different code.
  • You'll learn: The psychological root of OBOEs, systematic prevention checks, boundary condition testing, and when to use < vs <= with confidence.

Beginner-Friendly Explanations

Why Your Brain Defaults to Off-By-One Errors

Human counting (ordinal): 1st, 2nd, 3rd, 4th, 5th
Computer indexing (cardinal): 0, 1, 2, 3, 4

When someone says "the 5th element," your brain thinks index = 5. The computer knows it's index = 4.

Example:

typescript
const arr = ['a', 'b', 'c', 'd', 'e'];
// Human: "5 elements, so last is arr[5]"
// Computer: "Length is 5, so last is arr[4]"

console.log(arr[5]); // undefined (JavaScript) or IndexError (Python)

Why this is hard to unlearn: You've been counting from 1 since childhood. Zero-based indexing is artificial and unintuitive. Your default mental model conflicts with the programming model.

The Three Types of Off-By-One Errors

Type 1: Index Out of Bounds

typescript
for (let i = 0; i <= arr.length; i++) {  // BUG: i goes to arr.length
  console.log(arr[i]);
}
// Last iteration: arr[arr.length] is out of bounds

Type 2: Loop Runs Wrong Number of Times

typescript
// Want to run exactly N times
for (let i = 1; i <= n; i++) {  // Correct: runs N times (1, 2, ..., n)
for (let i = 0; i < n; i++) {   // Correct: runs N times (0, 1, ..., n-1)
for (let i = 0; i <= n; i++) {  // BUG: runs N+1 times

Type 3: Slice/Range Boundary Errors

typescript
// Want first N characters
const str = "hello";
str.substring(0, n);     // Correct: indices 0 to n-1
str.substring(0, n + 1); // BUG: includes index n

Each type has the same root: confusion about where boundaries are.

Step-by-Step Learning Guidance

Prevention Check 1: "What's the Last Valid Index?"

Before writing any loop, answer this question:

"If my array has n elements, what's the last valid index?"

Answer: n - 1 (always, in zero-indexed languages)

Application:

typescript
function sumArray(arr: number[]): number {
  let sum = 0;
  
  // Before writing the loop:
  // "arr has arr.length elements"
  // "Last valid index is arr.length - 1"
  // "So condition should be i < arr.length, NOT i <= arr.length"
  
  for (let i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
  return sum;
}

Mental checklist:

  • I know how many elements there are
  • I know the last valid index (count - 1)
  • My loop condition respects that boundary

Prevention Check 2: "How Many Times Should This Loop Run?"

Fence-post problem: You need a 10-meter fence with posts every meter. How many posts?

Intuitive answer: 10 posts (wrong)
Correct answer: 11 posts (one at 0m, 1m, ..., 10m)

In loops:

typescript
// Run exactly N times starting from index 0
for (let i = 0; i < n; i++) { ... }  // Runs at i = 0, 1, ..., n-1 (N times) ✓

// Run exactly N times starting from index 1
for (let i = 1; i <= n; i++) { ... }  // Runs at i = 1, 2, ..., n (N times) ✓

// Common mistake: mixing start point and boundary
for (let i = 0; i <= n; i++) { ... }  // Runs N+1 times ✗

Rule:

  • Start at 0? Use < endpoint
  • Start at 1? Use <= endpoint

Prevention Check 3: "Test Boundary Cases Mentally"

Before running code, trace with edge cases:

  1. Empty array (n = 0): Does the loop skip entirely?
  2. Single element (n = 1): Does the loop run exactly once?
  3. Exact boundary (accessing index n - 1): Is this valid?

Example:

typescript
function findMax(arr: number[]): number {
  let max = arr[0];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] > max) max = arr[i];
  }
  return max;
}

Mental trace:

  • Empty array (arr = []): max = arr[0] → undefined/error → Needs guard clause
  • Single element (arr = [5]): Loop runs 0 times, returns arr[0] = 5Correct
  • Two elements (arr = [3, 7]): Loop runs once (i = 1), accesses arr[1]Valid

Fixed code:

typescript
function findMax(arr: number[]): number {
  if (arr.length === 0) throw new Error("Empty array");
  
  let max = arr[0];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] > max) max = arr[i];
  }
  return max;
}

Prevention Check 4: "Inclusive or Exclusive?"

Many OBOEs happen in range operations (slicing, subarrays).

Question to always ask: Is the endpoint inclusive or exclusive?

Examples:

OperationRangeExplanation
arr.slice(start, end)[start, end)Includes start, excludes end
arr.substring(start, end)[start, end)Includes start, excludes end
for (let i = start; i < end; i++)[start, end)Includes start, excludes end
for (let i = start; i <= end; i++)[start, end]Includes both start and end

Rule: Most range operations use half-open intervals [start, end) where end is exclusive.

Why this matters:

typescript
const str = "hello";

// Want first 3 characters: "hel"
str.substring(0, 3); // Correct: [0, 3) includes indices 0, 1, 2
str.substring(0, 4); // Wrong: [0, 4) includes indices 0, 1, 2, 3 → "hell"

Visualizable Example: The Classic Mistake

Problem: Find the index of the last occurrence of a target value.

Buggy Code:

typescript
function lastIndexOf(arr: number[], target: number): number {
  for (let i = arr.length; i >= 0; i--) {  // BUG: i starts at arr.length
    if (arr[i] === target) return i;
  }
  return -1;
}

// Test: lastIndexOf([1, 2, 3, 2, 5], 2)
// Expected: 3
// Actual: Error or undefined

Why it's wrong:

code
arr = [1, 2, 3, 2, 5]
Indices: 0  1  2  3  4
Length: 5

Loop starts at i = 5
arr[5] is out of bounds (only indices 0-4 are valid)

Correct Code:

typescript
function lastIndexOf(arr: number[], target: number): number {
  for (let i = arr.length - 1; i >= 0; i--) {  // Fixed: start at last valid index
    if (arr[i] === target) return i;
  }
  return -1;
}

Prevention checklist applied:

  • Last valid index? arr.length - 1
  • Decrementing loop should start there
  • Boundary test: What if arr = [target]? Starts at i = 0, finds it, returns 0 ✓

Practical Preparation Strategies

Strategy 1: Use Descriptive Variable Names

Unclear:

typescript
for (let i = 0; i < n; i++) { ... }
// Is n the count or the last index?

Clear:

typescript
const count = arr.length;
for (let i = 0; i < count; i++) { ... }
// count is clearly the number of elements

Or:

typescript
const lastIndex = arr.length - 1;
for (let i = 0; i <= lastIndex; i++) { ... }
// lastIndex is clearly the final valid position

Why this helps: Naming forces you to clarify what the value represents, catching conceptual errors.

Strategy 2: Write Boundary Tests First

Before writing the main logic, write test cases for:

typescript
// Test function: reverseArray

// Boundary cases
reverseArray([]);           // Empty
reverseArray([1]);          // Single element
reverseArray([1, 2]);       // Two elements
reverse Array([1, 2, 3]);    // Odd count

// If any fail, you likely have an OBOE

This relates to manual code tracing—tracing boundary cases reveals OBOEs before runtime.

Strategy 3: Use Array Methods When Possible

Manual indexing (OBOE-prone):

typescript
let result = [];
for (let i = 0; i < arr.length; i++) {
  result.push(arr[i] * 2);
}

Array methods (OBOE-safe):

typescript
const result = arr.map(x => x * 2);

Why safer: You don't manually manage indices, eliminating the OBOE surface area.

When you must use manual indexing, apply the prevention checklist strictly.

Strategy 4: Create a Mental Decision Tree

When writing a loop, ask these in order:

code
1. Am I iterating over all elements?
   YES → for (let i = 0; i < arr.length; i++)
   NO → Go to 2

2. Am I starting from the end?
   YES → for (let i = arr.length - 1; i >= 0; i--)
   NO → Go to 3

3. Am I skipping some elements?
   → Define start and end carefully
   → Double-check with boundary cases

Common Mistakes to Avoid

Mistake 1: Fixing Without Understanding

You get an IndexError. You change <= to < randomly. It passes.

Problem: You didn't understand why <= was wrong. Next problem, you'll make the same mistake.

Fix: After every OBOE, write: "The bug was [specific issue]. It happened because [root cause]. To prevent it, I should [prevention rule]."

Example:

code
Bug: Loop condition was i <= arr.length
Root cause: arr.length is 5, but last valid index is 4. Loop tried to access arr[5].
Prevention: Always use i < arr.length for full iteration, or i <= lastValidIndex where lastValidIndex = arr.length - 1.

Mistake 2: Not Testing Edge Cases

You test with arr = [1, 2, 3, 4, 5] and it works.

Problem: Normal cases hide OBOEs. Bugs only appear at boundaries.

Fix: Always test:

  • Empty input
  • Single element
  • Exact boundary (e.g., if n=10, test with input size 10)

Mistake 3: Trusting Intuition Over Rules

You think, "10 elements means index 0-10." Your intuition is wrong, but you code from intuition.

Fix: When in doubt, write it out:

code
Array: [a, b, c, d, e]
Count: 5
Indices: 0, 1, 2, 3, 4
Last index: 5 - 1 = 4

This physical act overrides faulty intuition.

Mistake 4: Mixing 0-based and 1-based Logic

typescript
for (let i = 1; i < arr.length; i++) {  // BUG: skips arr[0]
  // Process arr[i]
}

Thought: "Start at 1, go to length."
Reality: Skipped the first element because you mixed counting (1-based) with indexing (0-based).

Fix: Stick to one scheme:

  • 0-based: for (let i = 0; i < n; i++)
  • 1-based: for (let i = 1; i <= n; i++) (only when iterating a count, not an array)

FAQ

Why do some languages/problems use 1-based indexing?
Some competitive programming judges (not LeetCode) and languages like Lua, MATLAB use 1-based indexing to match human intuition. For LeetCode (JavaScript, Python, Java, C++), always assume 0-based unless explicitly stated.

Should I always use < instead of <= in loops?
Not always. Use < count when iterating 0 to count-1. Use <= lastIndex when you've explicitly calculated the last valid position. The key is knowing which value you're comparing against.

How can I remember the difference between arr.slice(1, 4) and arr.splice(1, 4)?
Focus on slice for this context: slice(start, end) is [start, end) (end exclusive). splice is for mutation, different use case. Test boundary cases to internalize ranges.

What if I keep making OBOEs even after reading this?
That's normal. OBOEs are muscle memory issues. After each OBOE: (1) Identify which prevention check you skipped, (2) Write it down, (3) Next time, consciously apply that check. After 10-15 deliberate corrections, it becomes automatic.

Is there a tool that helps catch OBOEs before running?
Static analysis tools (ESLint with plugins, type checkers) catch some OBOEs. More effective: develop the habit of mental boundary checking. Tools like LeetCopilot's Chat Mode can guide you through debugging and help you spot boundary logic errors during practice.

Conclusion

You keep getting off-by-one errors not because you're careless, but because your brain's natural counting conflicts with zero-based indexing.

The fix isn't "be more careful." It's building systematic prevention checks that override faulty intuition:

  1. Before any loop: "What's the last valid index?"
  2. Before any iteration: "How many times should this run?"
  3. Before submitting: "Did I mentally trace empty, single-element, and boundary cases?"

These checks feel slow initially. That's the point. You're training your brain to think in zero-indexed terms instead of relying on flawed intuition.

Every OBOE is a learning opportunity. When you catch one, document why it happened and which check would have prevented it. After 10-15 deliberate corrections, the prevention checks become automatic, and OBOEs drop from "every other problem" to rare occurrences.

Your goal isn't perfection—you'll still make OBOEs occasionally, even experienced developers do. Your goal is building the debugging reflex that catches them before hitting "Run," and the systematic thinking that prevents them during coding.

The curse breaks when prevention becomes habit.

Want to Practice LeetCode Smarter?

LeetCopilot is a free browser extension that enhances your LeetCode practice with AI-powered hints, personalized study notes, and realistic mock interviews — all designed to accelerate your coding interview preparation.

Also compatible with Edge, Brave, and Opera

Related Articles