twigphp/Twig · error · RuntimeError
Argument " " is defined twice for macro " ".
Error message
Argument "%s" is defined twice for macro "%s".
What it means
Thrown by TwigMacro::callLegacy() when the same macro argument receives two values — once positionally and once by name (the named argument's index falls below the positional count). Twig detects this before invoking the macro body and raises a RuntimeError anchored to the calling template's line, instead of letting the value be silently overwritten.
Solutions
- Remove either the positional or the named value so each macro argument is supplied exactly once.
- Convert the whole call to named arguments to make duplicates obvious.
- If building arguments programmatically, deduplicate by normalized argument name before calling the macro.
Example fix
{# before #}
{{ macro.link('Homepage', text='Homepage') }}
{# after #}
{{ macro.link('Homepage') }} Defensive patterns
Strategy: validation
Validate before calling
// PHP: ensure no named arg collides with an occupied positional slot
function hasDuplicateMacroArgs(array $args, array $argumentIndexes): bool {
$positionalCount = count(array_filter(array_keys($args), 'is_int'));
foreach ($args as $k => $_) {
if (is_string($k) && isset($argumentIndexes[$k]) && $argumentIndexes[$k] < $positionCount()) {}
}
return false; // implement using $positionalCount comparison per TwigMacro
} Type guard
function findDuplicateMacroArg(array $args, array $argumentIndexes): ?string {
$positionalCount = 0;
foreach ($args as $k => $_) { if (is_int($k)) { $positionalCount++; } }
foreach ($args as $k => $_) {
if (is_string($k) && in_array($argumentIndexes[$k] ?? null, range(0, $positionalCount - 1), true)) {
return $k;
}
}
return null;
} Try / catch
try {
$out = $macro->callLegacy($args, $source, $lineno);
} catch (Twig\Error\RuntimeError $e) {
if (str_contains($e->getMessage(), 'is defined twice for macro')) {
error_log('Duplicate macro argument at '.$e->getTemplateLine().': '.$e->getMessage());
}
throw $e;
} Prevention
- When migrating a call to named arguments, delete the original positional value in the same edit.
- Use one style (all positional or all named) per macro call.
- Deduplicate programmatic argument arrays by normalized name before invocation.
When it happens
Trigger: Calling a macro with a value for argument N positionally and then also passing the same argument by name, e.g. {{ m('x', name='y') }} for a macro whose first parameter is $name.
Common situations: Refactoring a call to use named arguments but forgetting to remove the old positional value; dynamically assembled argument arrays where a name collides with an already-supplied positional slot; stricter Twig 4-style checks surfacing bugs that Twig 3 silently accepted.
Related errors
- The variadic argument
- The variadic argument
- Positional arguments cannot be used after named arguments…
- The "html_classes" function argument
- Block " " on template " " does not exist.
AI-assisted analysis of twigphp/Twig@a414c3a491 (2026-09-13).
Data as JSON: /api/errors/0368a6084aab2097.
Report an issue: GitHub.
Appendix: source
Thrown at src/TwigMacro.php:132
$sawNamed = true;
if (null === $i = $this->argumentIndexes[$key] ?? null) {
$hasUnknownNamed = true;
} else {
if (null === $duplicate && $i < $positionalCount) {
$duplicate = $key;
}
if (isset($this->requiredNames[$key])) {
++$namedRequired;
}
}
}
}
if ($misordered) {
throw new RuntimeError(\sprintf('Positional arguments cannot be used after named arguments for macro "%s".', $this->name), $lineno, $source);
}
if (null !== $duplicate) {
throw new RuntimeError(\sprintf('Argument "%s" is defined twice for macro "%s".', $duplicate, $this->name), $lineno, $source);
}
// For a fully named call, the coverage of the required arguments is exact; a
// mixed call falls back to the precise (and slower) per-argument check.
$mayMissRequired = 0 === $positionalCount
? $namedRequired < \count($this->requiredNames)
: $positionalCount < $this->requiredCount;
if ($mayMissRequired || (!$this->variadic && ($hasUnknownNamed || $positionalCount > \count($this->arguments)))) {
$this->triggerLegacyDeprecations($arguments, $positionalCount, $source, $lineno);
}
foreach ($this->renamedArguments as $name => $parameterName) {
if (\array_key_exists($name, $arguments)) {
$arguments[$parameterName] = $arguments[$name];
unset($arguments[$name]);
}
}View on GitHub (pinned to a414c3a491)