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

  1. Go to the reported line/column and remove or correct the offending character.
  2. Quote any literal text: move it outside the `{{ }}` expression or into a string.
  3. Verify the template file encoding is valid UTF-8 without stray control bytes.
  4. 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

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


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)