nodejs/node · error · TemplateSyntaxError

unexpected '%s'

Error message

unexpected '%s'

What it means

The lexer maintains a balancing_stack of expected closers as it sees { ( [. When it encounters }, ), or ] and the stack is empty, there is no matching opener — an unbalanced closing bracket — and it raises TemplateSyntaxError('unexpected \'%s\'').

Source

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

                            data = m.group(idx + 1)
                            if data or token not in ignore_if_empty:
                                yield lineno, token, data
                            lineno += data.count('\n')

                # strings as token just are yielded as it.
                else:
                    data = m.group()
                    # update brace/parentheses balance
                    if tokens == 'operator':
                        if data == '{':
                            balancing_stack.append('}')
                        elif data == '(':
                            balancing_stack.append(')')
                        elif data == '[':
                            balancing_stack.append(']')
                        elif data in ('}', ')', ']'):
                            if not balancing_stack:
                                raise TemplateSyntaxError('unexpected \'%s\'' %
                                                          data, lineno, name,
                                                          filename)
                            expected_op = balancing_stack.pop()
                            if expected_op != data:
                                raise TemplateSyntaxError('unexpected \'%s\', '
                                                          'expected \'%s\'' %
                                                          (data, expected_op),
                                                          lineno, name,
                                                          filename)
                    # yield items
                    if data or tokens not in ignore_if_empty:
                        yield lineno, tokens, data
                    lineno += data.count('\n')

                # fetch new position into new variable so that we can check
                # if there is a internal parsing error which would result
                # in an infinite loop
                pos2 = m.end()

View on GitHub (pinned to 1b2de5e052)

Solutions

  1. Find the stray closing bracket at the reported line and remove it or add its matching opener.
  2. Balance every ( [ { with its ) ] } in expressions and macros calls.

Example fix

{# before #}
{{ format_name(user)) }}

{# after #}
{{ format_name(user) }}
Defensive patterns

Strategy: validation

Validate before calling

def brackets_balanced(expr: str) -> bool:
    pairs = {')': '(', ']': '[', '}': '{'}
    stack = []
    for ch in expr:
        if ch in '([{':
            stack.append(ch)
        elif ch in pairs:
            if not stack or stack.pop() != pairs[ch]:
                return False
    return not stack

Try / catch

from jinja2.exceptions import TemplateSyntaxError
try:
    env.parse(source)
except TemplateSyntaxError as e:
    log.error('unbalanced bracket at %s:%s', name, e.lineno)

Prevention

When it happens

Trigger: A stray ), ], or } inside a Jinja2 expression/tag, e.g. {{ foo) }}, {% if (a and b) } %}, or an extra closing bracket from editing.

Common situations: Deleting an opening bracket while leaving its closer; copy-paste that introduces an unmatched closer; converting function-call syntax.

Related errors


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