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:
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
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 boundsType 2: Loop Runs Wrong Number of Times
// 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 timesType 3: Slice/Range Boundary Errors
// 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 nEach 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:
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:
// 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:
- Empty array (
n = 0): Does the loop skip entirely? - Single element (
n = 1): Does the loop run exactly once? - Exact boundary (accessing index
n - 1): Is this valid?
Example:
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, returnsarr[0] = 5→ Correct - Two elements (
arr = [3, 7]): Loop runs once (i = 1), accessesarr[1]→ Valid
Fixed code:
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:
| Operation | Range | Explanation |
|---|---|---|
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:
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:
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 undefinedWhy it's wrong:
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:
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 ati = 0, finds it, returns 0 ✓
Practical Preparation Strategies
Strategy 1: Use Descriptive Variable Names
Unclear:
for (let i = 0; i < n; i++) { ... }
// Is n the count or the last index?Clear:
const count = arr.length;
for (let i = 0; i < count; i++) { ... }
// count is clearly the number of elementsOr:
const lastIndex = arr.length - 1;
for (let i = 0; i <= lastIndex; i++) { ... }
// lastIndex is clearly the final valid positionWhy 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:
// 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 OBOEThis relates to manual code tracing—tracing boundary cases reveals OBOEs before runtime.
Strategy 3: Use Array Methods When Possible
Manual indexing (OBOE-prone):
let result = [];
for (let i = 0; i < arr.length; i++) {
result.push(arr[i] * 2);
}Array methods (OBOE-safe):
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:
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 casesCommon 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:
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:
Array: [a, b, c, d, e]
Count: 5
Indices: 0, 1, 2, 3, 4
Last index: 5 - 1 = 4This physical act overrides faulty intuition.
Mistake 4: Mixing 0-based and 1-based Logic
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:
- Before any loop: "What's the last valid index?"
- Before any iteration: "How many times should this run?"
- 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
