elastic/elasticsearch · error · IllegalArgumentException

[ #ARGUMENTS] has length [2] but [ #execute] takes [1]…

Error message

[{}#ARGUMENTS] has length [2] but [{}#execute] takes [1] argument.

What it means

After locating execute and reading the interface's PARAMETERS constant, ScriptClassInfo requires the PARAMETERS String[] length to equal the number of parameters on execute. A mismatch throws this IllegalArgumentException. NOTE: the message template hardcodes the literals [2] and [1] — these are NOT computed from the actual lengths, so the printed numbers are misleading regardless of the real mismatch.

Solutions

  1. Make PARAMETERS length exactly equal to the number of execute parameters.
  2. Order the PARAMETERS entries to match the execute parameter order — these become the variable names visible inside the script.
  3. Do not rely on the [2]/[1] numbers in the message; compare the actual array length against execute's arity yourself.

Example fix

// before
public interface MyScript {
    String[] PARAMETERS = {"params"};
    double execute(Map<String,Object> params, Map<String,DocValue> doc); // arity 2 != 1 -> 1344
}
// after
public interface MyScript {
    String[] PARAMETERS = {"params", "doc"};
    double execute(Map<String,Object> params, Map<String,DocValue> doc);
}
Defensive patterns

Strategy: validation

Validate before calling

void assertParametersMatch(Class<?> iface) throws Exception {
    String[] params = (String[]) iface.getField("PARAMETERS").get(null);
    long arity = java.util.Arrays.stream(iface.getMethods())
        .filter(m -> !m.isDefault() && m.getName().equals("execute"))
        .mapToInt(m -> m.getParameterTypes().length).findFirst().orElseThrow();
    if (params.length != arity)
        throw new IllegalStateException("PARAMETERS length " + params.length + " != execute arity " + arity);
}

Try / catch

try {
    new ScriptClassInfo(lookup, MyScript.class);
} catch (IllegalArgumentException e) {
    if (e.getMessage().contains("#ARGUMENTS] has length")) {
        fail("Align PARAMETERS[] length with execute() parameter count (message hardcodes [2]/[1], check real values)");
    }
    throw e;
}

Prevention

When it happens

Trigger: A custom script-context interface where the PARAMETERS String[] constant and the execute method parameter count disagree (e.g. PARAMETERS = {"a","b"} but execute takes one arg, or vice-versa).

Common situations: Adding/removing an execute parameter and forgetting to update PARAMETERS; copy-pasting a context definition; renaming a parameter without adjusting the array.

Related errors


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

Appendix: source

Thrown at modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptClassInfo.java:149

        MethodType methodType = MethodType.methodType(executeMethod.getReturnType(), executeMethod.getParameterTypes());
        this.executeMethod = new org.objectweb.asm.commons.Method(executeMethod.getName(), methodType.toMethodDescriptorString());
        executeMethodReturnType = definitionTypeForClass(
            painlessLookup,
            executeMethod.getReturnType(),
            componentType -> "Painless can only implement execute methods returning a whitelisted type but ["
                + baseClass.getName()
                + "#execute] returns ["
                + componentType.getName()
                + "] which isn't whitelisted."
        );

        // Look up the argument
        List<MethodArgument> arguments = new ArrayList<>();
        String[] argumentNamesConstant = readArgumentNamesConstant(baseClass);
        Class<?>[] types = executeMethod.getParameterTypes();
        if (argumentNamesConstant.length != types.length) {
            throw new IllegalArgumentException(
                "[" + baseClass.getName() + "#ARGUMENTS] has length [2] but [" + baseClass.getName() + "#execute] takes [1] argument."
            );
        }
        for (int arg = 0; arg < types.length; arg++) {
            arguments.add(methodArgument(painlessLookup, types[arg], argumentNamesConstant[arg]));
        }
        this.executeArguments = unmodifiableList(arguments);
        this.needsMethods = unmodifiableList(needsMethods);
        this.getMethods = unmodifiableList(getMethods);
        this.getReturns = unmodifiableList(getReturns);
        this.supportsCancellation = supportsCancellation(baseClass);
    }

    /**
     * Reflective check for whether a script base class opts into the persistent cancellation
     * mechanism by overriding {@code _getCancellationCheck()} with a non-default implementation
     * returning a {@code Runnable}.  Same semantics as {@link #supportsCancellation()} but
     * usable from places (e.g. {@link org.elasticsearch.painless.lookup.PainlessLookupBuilder})

View on GitHub (pinned to db6a809a66)