twigphp/Twig · error · SyntaxError

The variadic argument

Error message

The variadic argument "%s" in macro "%s" must be the last one.

What it means

In a macro signature, the variadic argument must be the final parameter. Twig throws this when a comma follows the variadic argument and another argument comes after it, e.g. '{% macro foo(...rest, extra) %}'.

Solutions

  1. Move the variadic argument to the end of the parameter list
  2. Rename and reorder parameters so all fixed arguments precede '...name'
  3. Split the macro into two if you genuinely need parameters after the catch-all

Example fix

// before
{% macro render(...attrs, class) %}
// after
{% macro render(class, ...attrs) %}
Defensive patterns

Strategy: validation

Validate before calling

// Ensure '...' argument is last in every macro signature
foreach ($macroSignatures as $sig) {
    $variadicPos = null;
    foreach ($sig as $i => $p) {
        if (str_starts_with($p, '...')) { $variadicPos = $i; }
    }
    if ($variadicPos !== null && $variadicPos !== count($sig) - 1) {
        throw new InvalidArgumentException('Variadic macro argument must be last.');
    }
}

Try / catch

try {
    $twig->render($template, $context);
} catch (SyntaxError $e) {
    error_log($e->getMessage() . ' at line ' . $e->getTemplateLine());
}

Prevention

When it happens

Trigger: Declaring '{% macro m(...args, name) %}'; in parseDefinition, after consuming the ',' the stream does not see ')' immediately, so the variadic is not last and a SyntaxError is raised.

Common situations: Adding a new parameter after an existing variadic when refactoring macros; porting signatures from languages that allow rest params anywhere.

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/69c02702a9234865. Report an issue: GitHub.

Appendix: source

Thrown at src/TokenParser/MacroTokenParser.php:104

                $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);
            }

            if (!$this->checkConstantExpression($default)) {
                throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext());
            }

View on GitHub (pinned to a414c3a491)