twigphp/Twig · error · SyntaxError

Unclosed " ".

Error message

Unclosed "%s".

What it means

At the end of Lexer::tokenize, after the EOF token is pushed, Twig checks the bracket stack (`$this->brackets`). Any still-open delimiter — `{%`, `{{`, or `(` — means the template ended before the construct was closed, so Twig throws this SyntaxError with the line number where the construct was opened. This is a compile-time (lexing) error caught before any rendering occurs.

Solutions

  1. Open the template at the line number given in the SyntaxError and add the missing closing delimiter (`%}`, `}}`, or `)`).
  2. Run `php bin/console lint:twig <dir>` (Symfony) or Twig's lint command to find all unclosed constructs across a template tree.
  3. Verify file integrity after deploys (checksum/size) to catch truncated template uploads.
  4. If templates are generated dynamically, validate each generated template with `$twig->parse($twig->tokenize(new \Twig\Source($code, $name)))` before writing it.

Example fix

// before (Twig)
{% if user.active %}
  Hello {{ user.name

// after
{% if user.active %}
  Hello {{ user.name }}
{% endif %}
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate template syntax before storing/deploying
try {
    $twig->parse($twig->tokenize(new \Twig\Source($code, $name)));
} catch (\Twig\Error\SyntaxError $e) {
    // reject template: 'Unclosed ...' at line N
}

Try / catch

try {
    $html = $twig->render('page.html.twig', $context);
} catch (\Twig\Error\SyntaxError $e) {
    if (str_contains($e->getMessage(), 'Unclosed "')) {
        // surface template name + line from $e->getSourceContext() / getTemplateLine()
    } else { throw $e; }
}

Prevention

When it happens

Trigger: A template ending with `{% if x %}` without `{% endif %}`... more precisely an unclosed `{%`/`{{` (e.g. `{% block x %}` opened via `{%` but the tag never closed with `%}`), or an unclosed `(` inside an expression reaching EOF. The stack records the expected closer and its opening line, producing 'Unclosed "(".', etc.

Common situations: Truncated template files (partial writes, bad deploys); editing errors that delete a closing `%}` or `}}`; templates built by string concatenation where a piece got dropped; long `{% verbatim %}`/comment blocks accidentally swallowing a closer.

Related errors


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

Appendix: source

Thrown at src/Lexer.php:260

                case self::STATE_VAR:
                    $this->lexVar();
                    break;

                case self::STATE_STRING:
                    $this->lexString();
                    break;

                case self::STATE_INTERPOLATION:
                    $this->lexInterpolation();
                    break;
            }
        }

        $this->pushToken(Token::EOF_TYPE);

        if ($this->brackets) {
            [$expect, $lineno] = array_pop($this->brackets);
            throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
        }

        return new TokenStream($this->tokens, $this->source);
    }

    private function lexData(): void
    {
        // if no matches are left we return the rest of the template as simple text token
        if ($this->position == \count($this->positions[0]) - 1) {
            $text = substr($this->code, $this->cursor);
            $this->pushToken(Token::TEXT_TYPE, $this->normalizeNewlines($text));
            $this->moveCursor($text);

            return;
        }

        // Find the first token after the current cursor
        $position = $this->positions[0][++$this->position];

View on GitHub (pinned to a414c3a491)