Fence Post Problem 🚧
The “Fence Post Problem” is a classic off-by-one error that frequently appears in computer science and programming. It describes a common scenario where the number of elements in a sequence (consider the “post” of a fence) differs from the number of intervals or connections between them (consider the sections of a fence). Typically, for n posts, there are n-1 sections. Misunderstanding this simple relationship can lead to bugs when calculating iterations, array bounds, or resource allocations.
If a fence has 3 post (⧘) it would need only 2 pieces of connecting wood between them (=) i.e. ⧘=⧘=⧘
| ⧘=⧘ | 2 post will need 1 connecting wood |
| ⧘=⧘=⧘ | 3 post will need 1 connecting wood |
| ⧘=⧘=⧘=⧘ | 4 post will need 3 connecting wood |
| ⧘=⧘=⧘=⧘=⧘ | 5 post will need 4 connecting wood |
| ⧘ | 1 post will need 0 connecting wood |
Similarly, if there 5 elements laid out, there would be 4 gaps between them and the total gap will be sum of the childGap value
total gap = (number of elements - 1) * childGap
Off-by-one bugs in the wild
They’re some of the oldest bugs in computing, and they keep showing up in fresh ways. Here are a few I keep running into, and see easily missed by people.
1. The half-open interval
The single most common cause of off-by-one in JavaScript is forgetting whether a range is inclusive or exclusive on each end.
// Pages 1 to 10, inclusive of both ends
for (let page = 1; page <= 10; page++) { ... } // 10 iterations ✅
// Items from index 0 to 10, exclusive of 10
for (let i = 0; i < 10; i++) { ... } // 10 iterations ✅
// Subtle: <= where it should be <
for (let i = 0; i <= arr.length; i++) { ... } // arr[arr.length] is undefined 💥
The convention most languages and APIs settle on is start-inclusive, end-exclusive ([start, end)). arr.slice(0, 3) returns three items, not four. range(1, 5) in Python gives you [1, 2, 3, 4]. Once you internalise it, a lot of bugs disappear.
2. Date ranges, “between” queries
Ask for “events between Jan 1 and Jan 31” and you’ll get a different answer depending on whether the bounds are inclusive on both ends or whether the end is exclusive midnight of the next day.
-- Misses anything on Jan 31 between 00:00:01 and 23:59:59
WHERE created_at BETWEEN '2025-01-01' AND '2025-01-31'
-- Correct half-open form
WHERE created_at >= '2025-01-01' AND created_at < '2025-02-01'
The BETWEEN form quietly drops 23 hours and 59 minutes of data.
3. Pagination — the “page index” question
Is the first page page 0 or page 1? Both are defensible. The bug appears when one part of the system thinks one way and another part thinks the other.
// API returns 0-indexed
GET /posts?page=0&size=20 // first 20 posts
// UI shows 1-indexed in the URL
/blog/page/1 // first 20 posts
// Translation layer in the middle
const apiPage = uiPage - 1;
That - 1 is a fence post in disguise. Forget it on one route handler and you’ll skip the first page (or fail to load the last one).
4. The Y2K and Y2038 problems
The most famous off-by-one bugs aren’t off-by-one in the loop sense — they’re “we ran out of representable values” bugs.
- Y2K — two-digit year fields wrapped from
99to00, which everyone interpreted as 1900 instead of 2000. - Y2038 — 32-bit signed
time_toverflows on January 19, 2038 at 03:14:07 UTC. The signed integer wraps to negative, and Unix timestamps suddenly point at 1901.
Both are off-by-one at the boundary of representation. The fix in both cases is widening the field. For Y2038, every modern OS has moved to 64-bit time_t, but embedded systems, old database columns, and forgotten ESP8266 firmware still ship with 32-bit timestamps. There’s a reason I keep checking.
5. The “less than” / “less than or equal” trap
This shows up everywhere - array bounds, retry loops, throttling.
// "Retry up to 3 times" - but this runs 4 times
for (let attempt = 0; attempt <= 3; attempt++) { ... }
// "Cap at 100" - but this lets 101 through
if (count < 100) increment();
// "Process the first N" - boundary depends on the indexer
for (let i = 0; i <= n - 1; i++) // works
for (let i = 0; i < n; i++) // works, less suspicious
for (let i = 1; i <= n; i++) // works, but be consistent
When in doubt, prefer the half-open form (i < n) - it composes better, plays nicely with length properties, and is what every other index-based API in the language already uses.
How to avoid them
A few habits that genuinely help —
- Pick a convention and stick to it. Half-open intervals are the right default. If you have to deviate, comment it.
- Reach for higher-level constructs.
arr.forEach,for...of,arr.slicepush the index maths into well-tested standard library code. Use them when the explicit index doesn’t add value. - Write the boundary cases as tests. “What happens when n=0?”, “What happens at the last index?”, “What about the day the clock rolls over?”. The bugs hide at the edges.
- Name the units in your variables.
endIndexExclusivevslastIndexis more verbose, but it’s also less ambiguous.
The fence post problem is small, almost trivial — until you realise that some version of it is in nearly every off-by-one bug ever shipped. Counting things is harder than it looks.