ArrayIndexOutOfBounds: Fix Java Index Errors
Fix ArrayIndexOutOfBoundsException fast: loop with < not <=, guard empty arrays, and traverse with enhanced-for by default...
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- ✓Basic Java arrays and loops
- ✓Reading stack traces
- ✓A JDK to compile examples
- ArrayIndexOutOfBoundsException means an index fell outside 0 to length-1: index equals length, negative math, or any access on an empty array
- Fix loop bounds first: use i < arr.length, never <=, and compute last as length-1 explicitly
- Guard arr[0] and arr[arr.length-1] with an emptiness check so zero-length arrays can't reach them
- Prevent the class with enhanced-for for reads and Objects.checkIndex for computed indexes
Think of hotel rooms numbered 0 to 9 — ten rooms, but there is no room 10. Asking for room 10 gets you a stare from the clerk: that's ArrayIndexOutOfBoundsException. Programmers keep asking for room 10 because they count ten rooms and forget numbering starts at zero. The fix is counting like the clerk: last room is always total minus one, and an empty hotel has no rooms at all.
The trace says ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 10, and line 43 reads data[i]. You counted ten elements, the loop ran ten times — so why is it asking for a slot that doesn't exist? Because valid indexes run 0 to 9, and your loop's final iteration asked for 10. Off by one. The most written bug in programming history, wearing its clearest error message.
What makes this exception deceptively tricky isn't the message — it's excellent — but the distance between the message and the cause. The index math that produced 10 might live three lines up, or in a size variable that changed, or in a second array with a different length than the one you looped over. Reading the number is step one; tracing where it was born is the actual fix.
This guide covers every shape: the == length classic, <= versus < loops, empty-array access, parallel arrays of mismatched length, negative indexes from bad arithmetic, and ragged 2D arrays. You'll get guard patterns, the enhanced-for escape hatch, and tests that pin boundaries. By the end, index errors die at write time instead of in production logs.
Index == Length: Why Ten Slots End at Nine
Arrays are zero-based: a length-10 array owns indexes 0 through 9, and index 10 belongs to nobody. The message Index 10 out of bounds for length 10 states this precisely — the code asked for one past the last slot. This happens when an index counts items (1-based thinking) instead of slots, when length is used as an index directly, or when a size variable from a different collection drives this array's access.
The one-line shape arr[arr.length] looks obviously wrong in isolation, yet it ships inside real code wearing disguises: last = list.size() then arr[last], or buffer[pos] where pos was incremented past the fill point. Any index derived from a count rather than a position deserves suspicion. The last valid index is always length - 1 — write that expression explicitly wherever you mean the tail.
The snippet shows the failure and its three cures: explicit length-1 for tail access, a < loop for traversal, and Objects.checkIndex for computed indexes. The last one is underused gold: it validates and returns the index in one call, throwing with a precise message when logic errs. The checkIndex call is underused gold: it validates and returns the index in one call with a precise message on failure. Write the tail expression explicitly wherever you mean the last slot of an array.
Loop Bounds: <= Versus <, Settled Forever
The canonical indexed loop is for (int i = 0; i < arr.length; i++) and any deviation needs justification. The <= variant runs length + 1 iterations, and its final arr[length] access throws — every time, deterministically. It survives review because readers see length and think complete, forgetting the start at zero already counts the first element. Drill the equivalence: i < length visits exactly indexes 0..length-1, which is every slot once.
Starting at 1 with <= length is the same bug in a mirror: it skips index 0 and throws at length. Code ported from 1-based languages carries this shape. Translate on sight: start 0, bound <, step 1. Backwards loops need care too — for (int i = arr.length - 1; i >= 0; i--) is correct; starting at length throws immediately.
When the loop transforms or filters, the output index can outrun the output array even with a correct read bound. Writing results compactly needs its own counter incremented only on writes. The snippet shows the correct loop family plus the separate-write-counter pattern that fixes the most common transformation bug. Drill the equivalence until it is reflex: i < length visits exactly indexes 0 to length-1, which is every slot once. Code ported from 1-based languages carries the mirrored shape, so translate it on sight.
Empty Arrays: arr[0] on Nothing
Zero-length arrays are legal objects with no valid indexes — even arr[0] throws. They arrive from searches with no matches, splits of empty strings, filters that reject everything, and varargs calls with no arguments. Code that grabs the first element optimistically works for years until the first empty result, then throws in a place that assumed non-emptiness.
Guard first-element access with an explicit length check carrying a message or a fallback. Which one depends on meaning: empty search results might legitimately return Optional.empty, while an empty batch at a stage that requires data should throw IllegalArgumentException naming the stage. The worst choice is catching the bounds exception after the fact — checking length is clearer, cheaper, and reviewable.
Varargs deserve a special note: method(String... args) with zero arguments gives a real empty array, not null. Any args[0] without a length check is a crash waiting for the first bare call. The snippet shows the guard trio: first-element access, varargs safety, and split-result handling — the three empty-array shapes you'll actually meet. The worst choice is catching the bounds exception after the fact; checking length is clearer, cheaper, and reviewable. Any args[0] without a length check is a crash waiting for the first bare call.
Enhanced-For: Delete the Index, Delete the Bug
Most index bugs exist because an index exists. When traversal needs no position — summing, printing, filtering, mapping — enhanced-for removes the variable that goes wrong: for (int v : arr) can't overshoot, can't use <=, can't mismatch lengths. The compiler writes a correct index or iterator underneath, and an entire bug class evaporates. This is the highest-leverage habit in the article: default to enhanced-for, reach for indexed loops only when you need positions.
Indexed loops stay necessary in four cases: writing to positions, walking two arrays in parallel, needing the index value itself, and iterating backwards with writes. In those cases keep the loop minimal — bound < length, body uses the index plainly — and extract anything clever into helpers. Cleverness inside indexed loops is where <= sneaks back in.
Parallel arrays deserve a warning of their own: two arrays walked by one index require equal lengths, and filters upstream love to break that invariant silently. Prefer a single array of records over parallel arrays wherever possible. The snippet shows the safe traversal default and the guarded parallel walk for the cases you can't restructure yet. Make enhanced-for the default and force indexed loops to justify themselves in comments during review. An entire bug class evaporates when no index variable exists to get wrong.
Negative Indexes and 2D Ragged Traps
Negative indexes throw just like oversized ones, and they're sneakier because the message shows -1 while readers hunt for overshoot. They come from unclamped subtraction (pos - window), from indexOf returning -1 fed straight into an array, and from modulo on negative dividends. Any index born of arithmetic needs a floor check: if the formula can yield -1, it will, on the input you didn't test.
Two-dimensional arrays add the ragged trap: Java's int[][] is an array of independent rows, each with its own length. Code assuming rectangular shape — looping columns to matrix[0].length for every row — throws on shorter rows. The fix is row-local bounds: matrix[r].length inside the row loop, never a cached width from row zero. Args arrays add a third trap: args[1] without checking args.length first throws on every short command line.
The snippet shows all three cures: indexOf guarded before use, ragged-safe nested loops, and args length checks. These are small, boring, and exactly what production requires. Any index born of arithmetic needs a floor check: if the formula can yield -1, it will, on the input you skipped in tests. Remember that rows are independent arrays, so bind columns to the row-local length every time.
Reading Index X out of bounds for length Y
This message is a full diagnosis in one line — learn to parse it instantly. The index tells you what was asked, the length tells you what existed, and their relationship names the bug. Index equals length means overshoot by one: <= loop or length-as-index. Index far beyond length means a stale size, a wrong array, or wild arithmetic. Negative index means unclamped math or an unchecked -1. Empty length with index 0 means an empty array reached first-element code.
The line number completes the picture: open it, list every index expression, and substitute the message's numbers. Usually one expression visibly yields the guilty value. When several could, log each candidate's value on a rerun — one line of output beats ten minutes of staring. Then walk that expression backward to where its inputs were born: the loop bound, the size variable, the arithmetic.
Lock the fix with boundary tests: full array, single element, empty array, and the exact size that failed. The failing size is the regression anchor — encode it as a test name like splitsChunkOf50000 so the next reader knows what it guards. Index bugs that get boundary tests never return. Index bugs that get boundary tests never return, because the exact failing size becomes a permanent regression anchor. Teach the team the three message shapes and half these pages close without escalation.
Off-by-One in Batch Split Corrupted 2.4M Records
- <= in an index loop is guilty until proven innocent. Review every <= against an array as a defect, and let enhanced-for remove the question.
- Catch blocks that skip work must reconcile counts. A job that exits zero while dropping rows is worse than one that crashes.
- Alert on warning-rate spikes, not just errors. The signal sat in warnings for 9 days because nobody wired them to a pager.
size()' src/main/java. Each hit is a suspect — verify whether the body indexes with the loop variable. Fix to < and run the boundary test with mvn -q -Dtest=ChunkTest test.| File | Command / Code | Purpose |
|---|---|---|
| io | public final class TailAccess { | Index == Length |
| io | public final class LoopBounds { | Loop Bounds |
| io | public final class EmptyGuards { | Empty Arrays |
| io | public final class Traverse { | Enhanced-For |
| io | public final class NegativeAndRagged { | Negative Indexes and 2D Ragged Traps |
Key takeaways
Common mistakes to avoid
6 patternsWriting <= in indexed loops
Indexing arr[0] without an emptiness check
Driving one array with another's length
Assuming rectangular 2D arrays
Feeding indexOf -1 into brackets
Catching the exception to skip work
Interview Questions on This Topic
What are the valid indexes of a length-N array?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's Exception Handling. Mark it forged?
5 min read · try the examples if you haven't