elastic/elasticsearch · error · IllegalArgumentException

invalid ring position

Error message

invalid ring position

What it means

Thrown by H3.hexRingPosToH3 when the effective position is not in [0,5]. For pentagons, positions >= 2 are shifted by +1 (to skip the missing k-axis direction), so a raw ringPos of 6 on a pentagon maps to effective 7 and is caught; negative positions always throw. Hexagons accept 0..5 directly.

Source

Thrown at libs/h3/src/main/java/org/elasticsearch/h3/H3.java:404

     * @param h3Address Origin index
     * @return the number of neighbor indexes from the origin
     */
    public static int hexRingSize(String h3Address) {
        return hexRingSize(stringToH3(h3Address));
    }

    /**
     * Returns the neighbor index at the given position.
     *
     * @param h3 Origin index
     * @param ringPos position of the neighbour index
     * @return the actual neighbour at the given position
     */
    public static long hexRingPosToH3(long h3, int ringPos) {
        // for pentagons, we skip direction at position 2
        final int pos = H3Index.H3_is_pentagon(h3) && ringPos >= 2 ? ringPos + 1 : ringPos;
        if (pos < 0 || pos > 5) {
            throw new IllegalArgumentException("invalid ring position");
        }
        return HexRing.h3NeighborInDirection(h3, HexRing.DIRECTIONS[pos].digit());
    }

    /**
     * Returns the neighbor index at the given position.
     *
     * @param h3Address Origin index
     * @param ringPos position of the neighbour index
     * @return the actual neighbour at the given position
     */
    public static String hexRingPosToH3(String h3Address, int ringPos) {
        return h3ToString(hexRingPosToH3(stringToH3(h3Address), ringPos));
    }

    /**
     * returns whether or not the provided hexagons border
     *

View on GitHub (pinned to db6a809a66)

Solutions

  1. Bound the loop by H3.hexRingSize(cell) (6 hex / 5 pentagon), never a hardcoded 6.
  2. Validate 0 <= ringPos < hexRingSize(cell) before calling.
  3. Remember pentagons have 5 ring positions; let hexRingSize drive the bound.

Example fix

// before
for (int i = 0; i <= 6; i++) { // throws on pentagons / on i=6
    long nb = H3.hexRingPosToH3(cell, i);
}

// after
for (int i = 0; i < H3.hexRingSize(cell); i++) {
    long nb = H3.hexRingPosToH3(cell, i);
}
Defensive patterns

Strategy: validation

Validate before calling

static long safeRingPos(long h3, int pos) {
    int max = org.elasticsearch.h3.H3.hexRingSize(h3); // 6 hex / 5 pentagon
    if (pos < 0 || pos >= max) {
        throw new IllegalArgumentException("ringPos " + pos + " out of [0," + max + ")");
    }
    return org.elasticsearch.h3.H3.hexRingPosToH3(h3, pos);
}

Type guard

static boolean validRingPos(long h3, int pos) {
    return pos >= 0 && pos < org.elasticsearch.h3.H3.hexRingSize(h3);
}

Try / catch

try {
    return H3.hexRingPosToH3(h3, pos);
} catch (IllegalArgumentException e) {
    if (e.getMessage().equals("invalid ring position")) {
        throw new IndexOutOfBoundsException("ringPos " + pos + " invalid for " + h3);
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling hexRingPosToH3(cell, n) with n negative, or n > 5 on a hexagon, or n > 5 on a pentagon (since pentagons remap n>=2 and thus n=6 -> effective 7 throws). The public H3.hexRing iterates 0..hexRingSize-1 (5 or 6), so direct callers passing unbounded n are the risk.

Common situations: Caller iterating 0..6 unconditionally instead of using hexRingSize; assuming 6 neighbors for pentagons (they have 5); passing a ring position sourced from a different H3 version's enumeration.

Related errors


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/8228a28a3fce9884. Report an issue: GitHub.