elastic/elasticsearch · error · IllegalArgumentException

Cannot iterate over [receiverClass]

Error message

Cannot iterate over [receiverClass]

What it means

The enhanced for loop on a def-typed variable requires the runtime type to be Iterable or an array. Def.lookupIterator checks both conditions and throws IllegalArgumentException if neither holds, because there is no way to produce an iterator from an arbitrary object.

Source

Thrown at modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java:1348

            MethodHandle iterator = ARRAY_TYPE_MH_MAPPING.get(arrayType);
            return iterator != null ? iterator : OBJECT_ARRAY_MH.asType(OBJECT_ARRAY_MH.type().changeParameterType(0, arrayType));
        }

        private ArrayIteratorHelper() {}
    }

    /**
     * Returns a method handle to do iteration (for enhanced for loop)
     * @param receiverClass Class of the array to load the value from
     * @return a MethodHandle that accepts the receiver as first argument, returns iterator
     */
    static MethodHandle lookupIterator(Class<?> receiverClass) {
        if (Iterable.class.isAssignableFrom(receiverClass)) {
            return OBJECT_ITERATOR;
        } else if (receiverClass.isArray()) {
            return ArrayIteratorHelper.newIterator(receiverClass);
        } else {
            throw new IllegalArgumentException("Cannot iterate over [" + receiverClass.getCanonicalName() + "]");
        }
    }

    // Conversion methods for def to primitive types.

    public static boolean defToboolean(final Object value) {
        if (value instanceof Boolean) {
            return (boolean) value;
        } else {
            throw castException(value.getClass(), boolean.class, null);
        }
    }

    public static byte defTobyteImplicit(final Object value) {
        if (value instanceof Byte) {
            return (byte) value;
        } else {
            throw castException(value.getClass(), byte.class, true);

View on GitHub (pinned to db6a809a66)

Solutions

  1. Verify the field mapping allows multiple values; if it does not, re-map or handle the scalar case separately.
  2. Guard iteration with an instanceof check: `if (x instanceof List) { for (def item : x) { ... } }`
  3. Wrap single values in a single-element List before iterating.
  4. Use an explicit type declaration (List, Map) instead of def so the compiler rejects non-iterable types at compile time.

Example fix

// before
def tags = doc['tag'].value;
for (def t : tags) {
    // process tag
}

// after
def tags = doc['tag'].value;
if (tags instanceof List) {
    for (def t : tags) {
        // process tag
    }
} else if (tags != null) {
    // handle single value
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Painless: verify the def value is iterable before using for-each
def x = doc['field'].value;
if (x instanceof List) {
    for (def item : x) {
        // process item
    }
} else if (x != null) {
    // handle single value
}

Type guard

// Painless type guard for iterable types
def isIterable(def value) {
    return value != null && (value instanceof Iterable || value.getClass().isArray());
}

Prevention

When it happens

Trigger: A Painless script iterates over a def variable whose runtime value is a scalar (Integer, String, Boolean, etc.). Example: `def x = doc['count'].value; for (def item : x) { ... }` where count resolves to an Integer.

Common situations: Iterating a document field that the script author assumed was an array, but the mapping stores scalar values for at least some documents. Processing search hits or aggregation buckets where a field is sometimes single-valued and sometimes absent.

Related errors


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