twigphp/Twig · error · SyntaxError
Unexpected character
Error message
Unexpected character "%s".
What it means
Inside an expression (`{{ ... }}` or `{% ... %}`), the Lexer encountered a character it cannot match against any known token regex (name, number, string, operator, punctuation). It reports the offending character plus line and column. This is Twig's fallback 'unlexable' branch in lexExpression.
Solutions
- Go to the reported line/column and remove or correct the offending character.
- Quote any literal text: move it outside the `{{ }}` expression or into a string.
- Verify the template file encoding is valid UTF-8 without stray control bytes.
- If migrating from another engine, convert foreign syntax (e.g. Handlebars helpers) to Twig equivalents.
Example fix
// before
{{ 100 € total }}
// after
{{ 100 }} € total Defensive patterns
Strategy: validation
Validate before calling
// pre-scan expression contents for characters outside Twig's token alphabet
if (preg_match('/\{\{[^}]*[@#$\\][^}]*\}\}/', $code, $m)) {
trigger_error('Suspicious character in expression: '.$m[0], E_USER_WARNING);
} Try / catch
try {
$twig->render($name, $context);
} catch (\Twig\Error\SyntaxError $e) {
log('Unexpected character at '.$e->getTemplateLine().':'.$e->getTemplateLine());
} Prevention
- Quote all literal text; keep punctuation outside {{ }} expressions.
- Ensure template files are valid UTF-8.
- Lint templates with twig-lint in the build pipeline.
When it happens
Trigger: Any character outside Twig's expression grammar appears inside a tag, e.g. '{{ 1 @ 2 }}', '{{ "unterminated }}', a stray backslash, or a mis-encoded/UTF-8 byte sequence that matches no token pattern.
Common situations: Typos like `{{ price }€}`; pasting code from other templating languages (Handlebars `{{#if}}`); broken multibyte characters after a bad encoding conversion; using reserved punctuation in variable names.
Related errors
- Unclosed " ".
- The variadic argument
- The variadic argument
- When using set, you must have the same number of variables…
- When using set with a block, you cannot have a multi-target.
AI-assisted analysis of twigphp/Twig@a414c3a491 (2026-09-13).
Data as JSON: /api/errors/0e60e6b1f05b9961.
Report an issue: GitHub.
Appendix: source
Thrown at src/Lexer.php:423
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes($this->normalizeNewlines(substr($match[0], 1, -1)), substr($match[0], 0, 1)));
$this->moveCursor($match[0]);
}
// opening double quoted string
elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
$this->brackets[] = ['"', $this->lineno];
$this->pushState(self::STATE_STRING);
$this->moveCursor($match[0]);
}
// inline comment
elseif (preg_match(self::REGEX_RAW_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) {
if (str_starts_with($match[0], '##')) {
$this->addDocumentation(substr($match[0], 2));
}
$this->moveCursor($match[0]);
}
// unlexable
else {
throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source, columnno: $this->source->getColumn($this->cursor));
}
}
private function stripcslashes(string $str, string $quoteType): string
{
$result = '';
$length = \strlen($str);
$i = 0;
while ($i < $length) {
if (false === $pos = strpos($str, '\\', $i)) {
$result .= substr($str, $i);
break;
}
$result .= substr($str, $i, $pos - $i);
$i = $pos + 1;
View on GitHub (pinned to a414c3a491)