Rectifying the ‘Number Exceeds Limitation’ Error in ID Hashing
As developers, we often face intricate challenges, and troubleshooting errors is a key part of the process. Recently, I came across an interesting problem related to a service that generates IDs and hashes them for user readability. Herein, I’ll share my experiences and solutions for this particular predicament.
Encounter with the Unexpected Error
java.lang.IllegalArgumentException: number can not be greater than 9007199254740992L at org.hashids.Hashids.encode(Hashids.java:141)
The error we faced was, “Number cannot be greater than 9007199254740992L”. The issue originated in the following piece of code:
Long hashId = (long) (random.nextDouble() * Math.pow(10, 6) * System.nanoTime() / ((long) Math.pow(10, 6)));
return HASH_IDS.encode(hashId);
Critical examination indicated that the fault lay within the `encode` function.
Tracing the Root of the Problem
An understanding of the essence of the issue required diving deeper into the architecture of our implementation and JavaScript (JS), where our reference implementation originates. The number limitation in JS is `(2⁵³ — 1)`, whilst in Java, we used `Long` values because `Integer` maxes out at `(2³¹ — 1)`. Despite `Long` having a theoretical limit up to `(2⁶³ — 1)`, to ensure consistency with the JS implementation, we had to restrict our upper bound to match that of JS, hence the specific number limitation.
Outlining the Temporary Fix
Given this issue occurred on a service live in production, we had to be quick on our toes to mitigate the error and minimize the impact. We implemented a retry mechanism to temporarily circumvent this issue, given that the hashId generation depended on system.nanoTime(). Below is the ‘quick fix’ code snippet we used:
Long number = (long) (Math.random() * Math.pow(10, 6) * System.nanoTime() / ((long) Math.pow(10, 6)));
int retries = 0;
int maxRetries = 10;
while (number > 9007199254740992L && retries < maxRetries) {
number = (long) (Math.random() * Math.pow(10, 6) * System.nanoTime() / ((long) Math.pow(10, 6)));
retries++;
}
return HASH_IDS.encode(number);
Proposing an Enduring Solution
However, for a long-term solution, I suggest replacing the hashing implementation with ULID (Universally Unique Lexicographically Sortable Identifiers). The ULID protocol provides reliable, robust and efficient generation of lexicographically sortable unique identifiers, dramatically reducing the risk of out-of-bounds errors by design. This Java library for ULID is available [here](https://github.com/f4b6a3/ulid-creator).
This particular error served as a detailed lesson in understanding the limitations of numerical representations across languages. It compelled us to search for comprehensive solutions, leading us to the ULID protocol, which not only provides a solution to our problem but also offers an efficient method for generating unique, sortable identifiers.