oracle/graal · error · IllegalArgumentException

methods with same signature

Error message

methods with same signature {} but incompatible return types: {} and others

What it means

Thrown by EspressoForeignProxyGenerator's return-type coverage check: when two proxied interfaces declare the same method signature but their return types are incompatible, the first failure mode triggers if the newly seen return type is primitive — a primitive can never be 'covered' by a reference return type, so the proxy cannot implement both methods and creation aborts. Message includes the friendly signature and the offending primitive type.

Solutions

  1. Do not proxy those two interfaces together — pick one, or wrap one behind an adapter object.
  2. Change one interface's method return type so they are assignable (e.g. both reference types with a common subtype).
  3. If you control the interfaces, rename the colliding method on one of them.

Example fix

// before
interface A { int fetch(); }
interface B { Object fetch(); }
proxy(A.class, B.class); // throws

// after
interface B { Object fetchValue(); } // renamed, no clash
proxy(A.class, B.class);
Defensive patterns

Strategy: validation

Validate before calling

// reject proxies over same-signature methods with primitive vs reference returns
Map<String, Klass> returnsBySignature = new HashMap<>();
for (Method m : allMethods(interfaces)) {
    String sig = m.getName() + Arrays.toString(m.getParameterTypes());
    Klass prev = returnsBySignature.putIfAbsent(sig, m.getReturnType());
    if (prev != null && !prev.isAssignableTo(m.getReturnType()) && !m.getReturnType().isAssignableTo(prev)) {
        throw new IllegalArgumentException("incompatible returns for " + sig);
    }
}

Try / catch

try {
    generator.getProxy(context, loader, interfaces);
} catch (IllegalArgumentException e) {
    // message contains the conflicting signature: split the proxy or fix the interfaces
}

Prevention

When it happens

Trigger: Proxying interfaces where one declares 'int m()' and another declares 'Object m()' (same name and parameter types): the primitive return type hits the isPrimitive() branch and throws. Replicates java.lang.reflect.Proxy's 'methods with same signature but incompatible return types' error.

Common situations: Cobbling together proxies over unrelated third-party interfaces that happen to share a method name; version drift where one interface's method signature changed its return type; bridge-style APIs mixing boxed and primitive returns.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/3323471f4bd9fef1. Report an issue: GitHub.

Appendix: source

Thrown at espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/nodes/interop/EspressoForeignProxyGenerator.java:731

    private static void checkReturnTypes(List<ProxyMethod> methods) {
        /*
         * If there is only one method with a given signature, there cannot be a conflict. This is
         * the only case in which a primitive (or void) return type is allowed.
         */
        if (methods.size() < 2) {
            return;
        }

        /*
         * List of return types that are not yet known to be assignable from ("covered" by) any of
         * the others.
         */
        LinkedList<Klass> uncoveredReturnTypes = new LinkedList<>();

        nextNewReturnType: for (ProxyMethod pm : methods) {
            Klass newReturnType = pm.returnType.getRawType();
            if (newReturnType.isPrimitive()) {
                throw new IllegalArgumentException(
                                "methods with same signature " +
                                                getFriendlyMethodSignature(pm.methodName,
                                                                pm.parameterTypes) +
                                                " but incompatible return types: " +
                                                newReturnType.getName() + " and others");
            }
            boolean added = false;

            /*
             * Compare the new return type to the existing uncovered return types.
             */
            ListIterator<Klass> liter = uncoveredReturnTypes.listIterator();
            while (liter.hasNext()) {
                Klass uncoveredReturnType = liter.next();

                /*
                 * If an existing uncovered return type is assignable to this new one, then we can
                 * forget the new one.

View on GitHub (pinned to a66e9ccd1d)