getgrav/grav · error · Twig\Error\SyntaxError

Expected endblock for block "%s" (but "%s" given).

Error message

Expected endblock for block "%s" (but "%s" given).

What it means

Grav's bundled Twig deferred extension supplies the block token parser used here. After parsing a block body, it accepts an optional name after {% endblock %} and verifies that it matches the opening block name; a mismatch raises Twig SyntaxError with both names and the source line.

Source

Thrown at system/src/Twig/DeferredExtension/DeferredTokenParser.php:52

        $deferred = $stream->nextIf(Token::NAME_TYPE, 'deferred');

        if ($deferred) {
            $block = new DeferredBlockNode($name, new EmptyNode(), $lineno);
        } else {
            $block = new BlockNode($name, new EmptyNode(), $lineno);
        }

        $this->parser->setBlock($name, $block);
        $this->parser->pushLocalScope();
        $this->parser->pushBlockStack($name);

        if ($stream->nextIf(Token::BLOCK_END_TYPE)) {
            $body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
            if ($token = $stream->nextIf(Token::NAME_TYPE)) {
                $value = $token->getValue();

                if ($value != $name) {
                    throw new SyntaxError(\sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext());
                }
            }
        } else {
            $body = new Nodes([
                new PrintNode($this->parser->parseExpression(), $lineno),
            ]);
        }
        $stream->expect(Token::BLOCK_END_TYPE);

        $block->setNode('body', $body);
        $this->parser->popBlockStack();
        $this->parser->popLocalScope();

        return new BlockReferenceNode($name, $lineno, $this->getTag());
    }

    public function decideBlockEnd(Token $token): bool
    {

View on GitHub (pinned to 6040efed04)

Solutions

  1. Change the name after {% endblock %} to exactly match the opening block.
  2. Remove the optional name and use plain {% endblock %}, which this parser accepts.
  3. Search the whole template chain, includes, and embedded strings for both names reported in the message.
  4. Run twig lint or compile templates in CI after large template refactors.

Example fix

// before
{% block hero %}
  <h1>{{ page.title }}</h1>
{% endblock intro %}

// after
{% block hero %}
  <h1>{{ page.title }}</h1>
{% endblock hero %}
{# or simply: {% endblock %} #}
Defensive patterns

Strategy: validation

Validate before calling

$source = file_get_contents($templateFile);
preg_match_all('/{%\s*(block|endblock)\b([^%}]*)%}/', $source, $matches, PREG_SET_ORDER);
$stack = [];
foreach ($matches as $tag) {
    if ($tag[1] === 'block') {
        preg_match('/([A-Za-z_]\w*)/', $tag[2], $name);
        $stack[] = $name[1] ?? null;
    } elseif ($stack) {
        preg_match('/([A-Za-z_]\w*)/', $tag[2], $name);
        $expected = array_pop($stack);
        if (($name[1] ?? $expected) !== $expected) {
            throw new InvalidArgumentException("endblock name does not match block {$expected}");
        }
    }
}

Try / catch

try {
    $twig->load($templateName);
} catch (SyntaxError $e) {
    return ['error' => 'Template syntax error: ' . $e->getMessage(), 'line' => $e->getLine()];
}

Prevention

When it happens

Trigger: Render a template containing {% block hero %}...{% endblock old_hero %}. The parser reaches DeferredTokenParser.php:48-53, sees endblock's name differs from hero, and throws before the template can compile.

Common situations: Copying and renaming a block but forgetting its endblock, merge conflicts that pair mismatched tags, nested block editing, and generated templates whose names are interpolated incorrectly.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/851ee6a06fcde7db. Report an issue: GitHub.