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
- 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
- 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
- Remove or correct the redefinition in the active profile (check both the POM and ~/.m2/settings.xml)
- 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
- Never define a property whose value references itself; use a literal default plus a -D override
- Lint POMs and settings profiles for property references and reject any node that appears in its own dependency chain
- When merging property maps (parent + profiles + CLI), assert the merge does not introduce a reference loop
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
- Cannot read project model from interpolating filter of seria
- Cannot evaluate expression '%s' for configuration entry '%s'
- Bad substitution operator in: ${variable}
- Repository list contains duplicate entries. Each repository
- A dependency has introduced a cycle
AI-assisted analysis of apache/maven@e4093d4e12 (2026-08-21).
Data as JSON: /api/errors/156e13c544af5c50.
Report an issue: GitHub.