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
- Open the template at the line number given in the SyntaxError and add the missing closing delimiter (`%}`, `}}`, or `)`).
- Run `php bin/console lint:twig <dir>` (Symfony) or Twig's lint command to find all unclosed constructs across a template tree.
- Verify file integrity after deploys (checksum/size) to catch truncated template uploads.
- 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
- Run twig lint (lint:twig) on every template before deploy
- Verify template file sizes/checksums after uploads to catch truncated files
- Never assemble templates by naive string concatenation; use includes/partials
- Pair every `{%`/`{{` opener with its closer before saving; editors with Twig syntax highlighting help
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
- Unexpected character
- An exception has been thrown during the compilation of a…
- Calling the "parent" function outside of a block is…
- Calling the "parent" function on a template that does not…
- Unexpected end of file: Unclosed "verbatim" block.
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)