twigphp/Twig · error · SyntaxError

The variadic argument

Error message

The variadic argument "%s" in macro "%s" cannot have a default value.

What it means

When parsing a macro definition, Twig rejects a variadic argument (declared with the '...' prefix) that is given a default value like '...args = []'. Variadic arguments collect all leftover call arguments, so a default would be meaningless and is therefore a syntax error.

Solutions

  1. Remove the '= default' from the variadic argument and rely on it capturing zero or more remaining arguments
  2. Move the default onto a preceding normal argument, or handle the empty case inside the macro body with a conditional
  3. Check the macro signature: only regular arguments may have defaults, and the variadic must come last

Example fix

// before
{% macro render(items, ...attrs = []) %}
// after
{% macro render(items, ...attrs) %}{% if attrs is empty %}...{% endif %}
Defensive patterns

Strategy: validation

Validate before calling

// Template-source check before rendering/compiling
if (preg_match('/\.\.\.\s*\w+\s*=/', $templateSource)) {
    throw new InvalidArgumentException('Variadic macro arguments must not have default values.');
}

Try / catch

try {
    $twig->parse($env->tokenize(new Source($template, 'tpl')));
} catch (SyntaxError $e) {
    // report $e->getMessage() with template line to the author
}

Prevention

When it happens

Trigger: Writing a macro parameter such as '{% macro foo(a, ...rest = ["x"]) %}'; the parser sees '=' after the variadic name in MacroTokenParser::parseDefinition and throws immediately.

Common situations: Copy-pasting ordinary argument syntax onto a variadic argument; misunderstanding Twig variadics after coming from PHP/JS where rest params sometimes appear with defaults in tutorials.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of twigphp/Twig@a414c3a491 (2026-09-13). Data as JSON: /api/errors/2b9aa75007707e19. Report an issue: GitHub.

Appendix: source

Thrown at src/TokenParser/MacroTokenParser.php:101

        $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
        while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
            if (\count($arguments)) {
                $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');

                // if the comma above was a trailing comma, early exit the argument parse loop
                if ($stream->test(Token::PUNCTUATION_TYPE, ')')) {
                    break;
                }
            }

            if ($stream->nextIf(Token::OPERATOR_TYPE, '...')) {
                $token = $stream->expect(Token::NAME_TYPE, null, 'A variadic argument must be a name');
                $variadicName = (new LocalVariable($token->getValue(), $token->getLine()))->getAttribute('name');
                if (str_starts_with($variadicName, TempNameExpression::RESERVED_NAME_PREFIX)) {
                    $variadicName = substr($variadicName, \strlen(TempNameExpression::RESERVED_NAME_PREFIX));
                }
                if ($stream->test(Token::OPERATOR_TYPE, '=')) {
                    throw new SyntaxError(\sprintf('The variadic argument "%s" in macro "%s" cannot have a default value.', $variadicName, $macroName), $token->getLine(), $stream->getSourceContext());
                }
                if ($stream->nextIf(Token::PUNCTUATION_TYPE, ',') && !$stream->test(Token::PUNCTUATION_TYPE, ')')) {
                    throw new SyntaxError(\sprintf('The variadic argument "%s" in macro "%s" must be the last one.', $variadicName, $macroName), $token->getLine(), $stream->getSourceContext());
                }

                break;
            }

            $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
            $name = new LocalVariable($token->getValue(), $token->getLine());
            NodeDocumentation::add($name, $token);
            if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) {
                $default = $this->parser->parseExpression();
            } else {
                $default = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());
                $default->setAttribute('is_implicit', true);
            }

View on GitHub (pinned to a414c3a491)