nodejs/node · error · TemplateSyntaxError

unexpected char %r at %d

Error message

unexpected char %r at %d

What it means

The tokenizer loops over a set of regex rules to advance through the source. If no rule matches at the current position and the position is not at end-of-source, the character is untokenizable and Jinja2 raises TemplateSyntaxError('unexpected char %r at %d') with the offending character and byte offset.

Source

Thrown at tools/inspector_protocol/jinja2/lexer.py:737

                        stack.append(new_state)
                    statetokens = self.rules[stack[-1]]
                # we are still at the same position and no stack change.
                # this means a loop without break condition, avoid that and
                # raise error
                elif pos2 == pos:
                    raise RuntimeError('%r yielded empty string without '
                                       'stack change' % regex)
                # publish new function and start again
                pos = pos2
                break
            # if loop terminated without break we haven't found a single match
            # either we are at the end of the file or we have a problem
            else:
                # end of text
                if pos >= source_length:
                    return
                # something went wrong
                raise TemplateSyntaxError('unexpected char %r at %d' %
                                          (source[pos], pos), lineno,
                                          name, filename)

View on GitHub (pinned to 1b2de5e052)

Solutions

  1. Inspect the character and offset reported in the message and remove or escape it.
  2. Strip non-ASCII/control characters from the template source (e.g. replace smart quotes and non-breaking spaces).
  3. If the character is intentional inside text, move it out of an expression/tag into literal template text.

Example fix

{# before #}
{{ user.@name }}

{# after #}
{{ user.name }}
Defensive patterns

Strategy: validation

Validate before calling

import unicodedata

def normalize_template_source(source: str) -> str:
    # replace smart quotes / non-breaking spaces that confuse the lexer
    source = source.replace('\u201c', '"').replace('\u201d', '"')
    source = source.replace('\u2018', "'").replace('\u2019', "'")
    source = source.replace('\u00a0', ' ')
    if any(unicodedata.category(ch) == 'Cc' and ch not in '\n\t' for ch in source):
        raise ValueError('control characters found in template source')
    return source

Try / catch

from jinja2.exceptions import TemplateSyntaxError
try:
    env.parse(source)
except TemplateSyntaxError as e:
    if 'unexpected char' in str(e):
        log.error('remove/escape the offending character at offset %s', e.lineno)
    raise

Prevention

When it happens

Trigger: A character that does not begin any valid token at that point — for example an unescaped special symbol inside an expression, a stray $ or @ (outside Jinja2's grammar), a control character, or invalid operator combinations.

Common situations: Pasting code containing syntax from another template language (Liquid, Twig, Mustache); invisible/control characters in the source; non-breaking spaces or smart quotes copied from a word processor.

Related errors


AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13). Data as JSON: /api/errors/812c2cc384c0a669. Report an issue: GitHub.