apache/maven · error · InterpolatorException

recursive variable reference: ${variable}

Error message

recursive variable reference: ${variable}

What it means

Thrown by Maven's DefaultInterpolator while expanding ${...} placeholders in a POM or settings. resolveVariable() tracks every variable currently being resolved in a cycleMap; if resolving a variable requires the same variable again (directly, like <a>${a}</a>, or through a chain like a=${b}, b=${a}), the duplicate cycleMap.add() fails and InterpolatorException reports the offending variable name. The check is what stops model interpolation from recursing forever.

Source

Thrown at impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultInterpolator.java:372

            } else {
                substValue = MARKER + "{" + variable + "}";
            }
        }

        return substValue;
    }

    private static String resolveVariable(
            String variable,
            Set<String> cycleMap,
            Map<String, String> configProps,
            UnaryOperator<String> callback,
            BinaryOperator<String> postprocessor,
            boolean defaultsToEmptyString) {

        // Verify that this is not a recursive variable reference
        if (!cycleMap.add(variable)) {
            throw new InterpolatorException("recursive variable reference: " + variable);
        }

        String substValue = null;
        // Try configuration properties first
        if (configProps != null) {
            substValue = configProps.get(variable);
        }
        if (substValue == null && !variable.isEmpty() && callback != null) {
            String s1 = callback.apply(variable);
            String s2 =
                    doSubstVars(s1, variable, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
            substValue = postprocessor != null ? postprocessor.apply(variable, s2) : s2;
        }

        // Remove the variable from cycle map
        cycleMap.remove(variable);
        return substValue;
    }

View on GitHub (pinned to e4093d4e12)

Solutions

  1. Search pom.xml, parent poms, settings.xml profiles and -D overrides for the variable named in the message and break the self- or mutual reference
  2. If the intent was a default value, put the literal default in the property and override it from the CLI (-Dname=value) instead of self-referencing
  3. Remove or correct the redefinition in the active profile (check both the POM and ~/.m2/settings.xml)
  4. Run with -X -e to see which model and which property triggered the interpolation chain

Example fix

<!-- before -->
<properties>
  <docker.tag>${docker.tag}</docker.tag>
</properties>

<!-- after -->
<properties>
  <docker.tag>latest</docker.tag>
</properties>
<!-- override at will with: mvn deploy -Ddocker.tag=1.2.3 -->
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check a property map for cycles before interpolating
static List<String> findPropertyCycle(Map<String, String> props) {
    for (String start : props.keySet()) {
        Set<String> seen = new LinkedHashSet<>();
        String cur = start;
        while (cur != null) {
            if (!seen.add(cur)) {
                return new ArrayList<>(seen); // cycle: last element repeats
            }
            String v = props.get(cur);
            Matcher m = Pattern.compile("\\$\\{([^}]+)\\}").matcher(v == null ? "" : v);
            cur = m.find() ? m.group(1) : null;
        }
    }
    return List.of();
}

Try / catch

try {
    String out = interpolator.interpolate(input, ctx);
} catch (InterpolatorException e) {
    // e.getMessage() names the recursive variable; fall back to the raw value or abort
    log.warn("Unresolvable expression, keeping raw value: {}", e.getMessage());
    return input;
}

Prevention

When it happens

Trigger: A property whose value contains its own placeholder, or two or more properties that reference each other in a ring, hit while building the effective model, interpolating profile activation conditions, or resolving ${...} expressions in dependencies/versions.

Common situations: Copy-pasted property blocks in pom.xml or settings.xml profiles; a profile that redefines a property to itself to 'keep the default'; CI injecting a -D override that loops back to the same name; property chains introduced during a Maven upgrade.

Related errors


AI-assisted analysis of apache/maven@e4093d4e12 (2026-08-21). Data as JSON: /api/errors/156e13c544af5c50. Report an issue: GitHub.