Math.abs(Integer.MIN_VALUE) Trap: The Shard Router That Crashes on One User in Four Billion2026-08-27
This sharding class routes users to one of N caches based on their user ID. It ran flawlessly for months across billions of requests, then started throwing ArrayIndexOutOfBoundsException: -2 for a single stubborn customer whose retries never went away.
public class ShardRouter {
private final Cache[] shards;
public ShardRouter(int shardCount) {
this.shards = new Cache[shardCount];
for (int i = 0; i < shardCount; i++) {
shards[i] = new Cache();
}
}
public Cache shardFor(String userId) {
int hash = userId.hashCode();
// hashCode can be negative; take absolute value first
int index = Math.abs(hash) % shards.length;
return shards[index];
}
public void put(String userId, String value) {
shardFor(userId).store(userId, value);
}
}
The logic looks airtight: hash the ID, take the absolute value to guarantee non-negativity, then modulo down to a valid array index. What's the negative index doing there?
Math.abs(int) has a documented but widely-forgotten failure mode: it can return a negative number. Specifically, Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE. The reason is two's-complement asymmetry — int can represent -2,147,483,648 but not +2,147,483,648. Negating MIN_VALUE overflows and wraps right back to itself, silently, with no exception.
Then Java's % operator preserves the sign of the dividend: Integer.MIN_VALUE % 10 is -8, not 8. So when a user ID happens to hash to exactly Integer.MIN_VALUE, the "safe" absolute-value guard passes it through unchanged, and the modulo emits a negative index. Array access explodes.
How rare is this? For a well-distributed hash, about 1 in 4.3 billion strings triggers it — so it can hide in tests forever and only surface once you're at scale. Once a user's ID lands there, it lands there every request; that customer's traffic never succeeds again until their ID changes.
The pattern Math.abs(x) % n is a code-review red flag anywhere it appears. The fix is Math.floorMod, which does the arithmetically correct thing for negative dividends without an intermediate absolute value:
public Cache shardFor(String userId) {
int hash = userId.hashCode();
int index = Math.floorMod(hash, shards.length);
return shards[index];
}
Alternatives that also work: mask off the sign bit with (hash & Integer.MAX_VALUE) % shards.length — this loses one bit of entropy but never overflows — or promote to long before calling abs: (int)(Math.abs((long) hash) % shards.length). Avoid ((hash % n) + n) % n; it's correct but easy to typo.
The deeper lesson is that two's-complement integer types have exactly one value whose negation isn't representable, and every "make it positive" idiom you'll ever write needs to survive that value. The same trap lives in C (abs, llabs), C++ (std::abs on int), Rust (i32::abs panics in debug, wraps in release), and Go (no builtin — you have to write it, and most people write it wrong). Anywhere you see abs followed by % or array indexing, mentally substitute MIN_VALUE and see what happens.
Math.abs(Integer.MIN_VALUE) is negative — use Math.floorMod for modular arithmetic on possibly-negative integers, and treat any abs(x) % n pattern as a latent bug waiting for a one-in-four-billion input.
