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
- Add the missing closing tag named in the error (e.g. append `{% endif %}`).
- Re-check nesting by matching each open block to its closer, innermost first.
- Beware `{% comment %}`/`{% raw %}` swallowing closers — move them so they don't wrap other blocks' end tags.
- 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
- Close each block immediately after opening it, then fill in the body.
- Don't wrap other blocks' end tags inside comment/raw blocks.
- Validate generated/assembled templates before shipping.
- Keep templates short; split into partials so pairs stay visible.
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
- errors.syntax.unexpected_else (locale: Unexpected 'else'…
- errors.syntax.invalid_delimiter
- errors.syntax.unknown_tag (locale: 'Unknown tag 'tag'')
- errors.syntax.tag_termination
- errors.syntax.variable_termination
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 publicView on GitHub (pinned to 807d45a6b3)