Shopify/liquid · error · Liquid::SyntaxError

errors.syntax.tag_never_closed

Error message

errors.syntax.tag_never_closed (locale: "'{block_name}' tag was never closed")

What it means

Liquid raises this SyntaxError at the end of parsing when a block tag that requires a closing tag (if, for, capture, raw, custom blocks...) was never terminated. parse_body calls raise_tag_never_closed when the token stream ends before the block's delimiter appears, reporting which tag was left open.

Solutions

  1. Add the missing closing tag named in the error (e.g. append `{% endif %}`).
  2. Re-check nesting by matching each open block to its closer, innermost first.
  3. Beware `{% comment %}`/`{% raw %}` swallowing closers — move them so they don't wrap other blocks' end tags.
  4. For generated templates, validate the assembled string parses before deploying.

Example fix

// before
{% for item in items %}
  {{ item.name }}
// after
{% for item in items %}
  {{ item.name }}
{% endfor %}
Defensive patterns

Strategy: validation

Validate before calling

OPENS = %w[if for capture case unless raw tablerow comment form paginate]
stack = []
src.scan(/{%-?\s*(\w+)/).flatten.each do |t|
  if OPENS.include?(t) then stack.push(t)
  elsif t == 'else' || t == 'elsif' then next
  elsif t.start_with?('end') then stack.pop
  end
end
puts "unclosed: #{stack}" unless stack.empty?

Type guard

null

Try / catch

begin
  Liquid::Template.parse(src)
rescue Liquid::SyntaxError => e
  if e.message =~ /was never closed/
    logger.error("unclosed liquid tag: #{e.message}")
    fallback_template
  else
    raise
  end
end

Prevention

When it happens

Trigger: Omitting `{% endif %}`, `{% endfor %}`, `{% endcapture %}` etc.; template truncated by a bad include or editor save; an inner tag consuming the outer closer so the outer block reaches EOF; EOF inside a custom block.

Common situations: Long templates where the close is far from the open; comment tags hiding the closer; generated templates from string concatenation dropping the last lines; conditional includes that each open blocks but only some close them.

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/8dcb4816d26b9bd7. Report an issue: GitHub.

Appendix: source

Thrown at lib/liquid/block.rb:56

      if tag == 'else'
        raise SyntaxError, parse_context.locale.t(
          "errors.syntax.unexpected_else",
          block_name: block_name,
        )
      elsif tag.start_with?('end')
        raise SyntaxError, parse_context.locale.t(
          "errors.syntax.invalid_delimiter",
          tag: tag,
          block_name: block_name,
          block_delimiter: block_delimiter,
        )
      else
        raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
      end
    end

    def raise_tag_never_closed(block_name)
      raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
    end

    def block_name
      @tag_name
    end

    def block_delimiter
      @block_delimiter ||= "end#{block_name}"
    end

    private

    # @api public
    def new_body
      parse_context.new_block_body
    end

    # @api public

View on GitHub (pinned to 807d45a6b3)