Shopify/liquid · error · Liquid::SyntaxError

Expected # but found #

Error message

Expected #{type} but found #{@tokens[@p].first}

What it means

Liquid::Parser#consume raises Liquid::SyntaxError when the next token's type does not match the expected type (or when a specific type was requested but the token stream has something else). It signals malformed Liquid syntax during expression parsing, e.g. inside variables or variable_lookups.

Solutions

  1. Fix the Liquid template so the expected token is present (e.g. complete the identifier after the dot)
  2. Validate templates with Liquid::Template.parse in a test/CI step to catch syntax errors early
  3. Check for missing or extra dots, brackets, and pipes in the failing expression

Example fix

<!-- before -->
{{ product. }}
<!-- after -->
{{ product.title }}
Defensive patterns

Strategy: validation

Validate before calling

def valid_liquid?(tpl)
  Liquid::Template.parse(tpl)
  true
rescue Liquid::SyntaxError
  false
end

Try / catch

begin
  Liquid::Template.parse(template_source)
rescue Liquid::SyntaxError => e
  logger.warn("Liquid syntax error: #{e.message}")
end

Prevention

When it happens

Trigger: A template expression like {{ product. }} or {{ a[ }} where an identifier, dot, or closing bracket is expected but a different token (or end of stream) is found; malformed range or argument syntax.

Common situations: Typo in a template (missing variable name after a dot); copy-pasted broken Liquid; dynamically generated templates with string interpolation errors; unclosed brackets.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of Shopify/liquid@807d45a6b3 (2026-09-08). Data as JSON: /api/errors/2b0890e5437a723c. Report an issue: GitHub.

Appendix: source

Thrown at lib/liquid/parser.rb:19

# frozen_string_literal: true

module Liquid
  class Parser
    def initialize(input, reject_bare_brackets: false)
      ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
      @tokens = Lexer.tokenize(ss)
      @p      = 0 # pointer to current location
      @reject_bare_brackets = reject_bare_brackets
    end

    def jump(point)
      @p = point
    end

    def consume(type = nil)
      token = @tokens[@p]
      if type && token[0] != type
        raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
      end
      @p += 1
      token[1]
    end

    # Only consumes the token if it matches the type
    # Returns the token's contents if it was consumed
    # or false otherwise.
    def consume?(type)
      token = @tokens[@p]
      return false unless token && token[0] == type
      @p += 1
      token[1]
    end

    # Like consume? Except for an :id token of a certain name
    def id?(str)
      token = @tokens[@p]

View on GitHub (pinned to 807d45a6b3)