elastic/elasticsearch · error · IllegalStateException

Classes from a previous version not found: ${deletedClasses}

Error message

Classes from a previous version not found: ${deletedClasses}

What it means

Thrown as IllegalStateException by JarScanner.compareSignatures() when the set of class names in the old (reference) jar is not a subset of the class names in the new jar. Removing a public class from a stable-API jar is a backwards-incompatible change; the comparison treats any deleted class as a violation. The error lists the deleted class names so the developer knows exactly what was removed.

Source

Thrown at build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/JarApiComparisonTask.java:212

        /**
         * Comparison: The signatures are maps of class names to public class, field, or method
         * declarations.
         * </p>
         * First, we check that the new jar signature contains all the same classes
         * as the old jar signature. If not, we return an error.
         * </p>
         * Second, we iterate over the signature for each class. If a signature from the old
         * jar is absent in the new jar, we add it to our list of errors.
         * </p>
         * Note that it is fine for the new jar to have additional elements, as this
         * is backwards compatible.
         */
        public static void compareSignatures(Map<String, Set<String>> oldSignature, Map<String, Set<String>> newSignature) {
            Set<String> deletedClasses = new HashSet<>(oldSignature.keySet());
            deletedClasses.removeAll(newSignature.keySet());
            if (deletedClasses.size() > 0) {
                throw new IllegalStateException("Classes from a previous version not found: " + deletedClasses);
            }

            Map<String, Set<String>> deletedMembersMap = new HashMap<>();
            for (Map.Entry<String, Set<String>> entry : oldSignature.entrySet()) {
                Set<String> deletedMembers = new HashSet<>(entry.getValue());
                deletedMembers.removeAll(newSignature.get(entry.getKey()));
                if (deletedMembers.size() > 0) {
                    deletedMembersMap.put(entry.getKey(), Set.copyOf(deletedMembers));
                }
            }
            if (deletedMembersMap.size() > 0) {
                throw new IllegalStateException(
                    "Classes from a previous version have been modified, violating backwards compatibility: " + deletedMembersMap
                );
            }
        }
    }
}

View on GitHub (pinned to db6a809a66)

Solutions

  1. For each deleted class in the error, decide: was the removal intentional? If not, restore the class or add a deprecated stub.
  2. If a class was moved/renamed, keep a deprecated forwarding class at the old location to preserve binary compatibility.
  3. If the removal is intentional and the API check should be updated, bump the stable API baseline reference jar to the current version (acknowledging the break).
  4. Verify the new jar build includes all expected classes — check the jar task's includes/excludes and source set.

Example fix

// before: public class Foo removed from stable API
// old jar: org/elasticsearch/Foo.class
// new jar: (missing) → throws

// after: keep a deprecated stub for binary compatibility
package org.elasticsearch;
@Deprecated(forRemoval = true)
public class Foo {
    public Foo() { /* deprecated shim */ }
}
Defensive patterns

Strategy: try-catch

Validate before calling

Set<String> deleted = new HashSet<>(oldSignature.keySet());
deleted.removeAll(newSignature.keySet());
if (deleted.isEmpty() == false) {
    System.err.println("API break — deleted classes: " + deleted);
    // decide: restore classes, bump baseline, or document intentional removal
}

Try / catch

try {
    JarScanner.compareSignatures(oldJS.jarSignature(), newJS.jarSignature());
} catch (IllegalStateException e) {
    // e.getMessage() lists deleted classes/members
    throw new GradleException("Stable API backwards-compatibility violation: " + e.getMessage(), e);
}

Prevention

When it happens

Trigger: compareSignatures (line 208) computes deletedClasses = oldSignature.keySet() - newSignature.keySet(). If non-empty (line 211), it throws with the set. Each entry is a class path (e.g., 'org/elasticsearch/X.class') present in the old jar but absent from the new. Triggered by deleting a public class, moving it to a different package, or renaming it.

Common situations: A public stable-API class was removed or renamed between versions; a class was moved to a different package (counts as delete + add); a class was made non-public or excluded from the jar; the new jar build accidentally excluded a package; refactoring merged a class into another without keeping the old name.

Related errors


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