Shopify/liquid · error · Liquid::UndefinedVariable
undefined variable #
Error message
undefined variable #{key} What it means
Liquid raises Liquid::UndefinedVariable during lookup_and_evaluate when strict_variables mode is on and the requested key does not exist in the object being looked up (and the object responds to key?). This makes missing variables a hard error instead of nil, so typos in variable names fail fast.
Solutions
- Define/assign the missing variable before use: `{% assign key = ... %}` or pass it in the render assigns hash.
- Pass the variable via `Template#render('key' => value)` or Environment data so lookups succeed.
- For optional variables, use `raise_on_not_found: false` in custom lookups or guard with `{% if key %}` — note strict mode still treats plain nil as error unless handled.
- If strict_variables is too aggressive for a template, render that template with strict_variables: false.
Example fix
// before
Liquid::Template.parse("{{ user.name }}").render # strict_variables: true, user missing
// after
template.render({ "user" => { "name" => "Ada" } }, strict_variables: true) Defensive patterns
Strategy: try-catch
Validate before calling
required = %w[user cart]
missing = required - assigns.keys
raise "missing template vars: #{missing.join(',')}" unless missing.empty? Type guard
def variable_defined?(assigns, key) assigns.is_a?(Hash) && assigns.key?(key) end
Try / catch
begin
html = template.render(assigns, strict_variables: true)
rescue Liquid::UndefinedVariable => e
logger.warn("missing template variable: #{e.message}")
html = template.render(assigns, strict_variables: false)
end Prevention
- Enable strict_variables in tests to catch typos early; relax only in render paths with optional vars.
- Pass all required variables explicitly via the render assigns hash.
- Guard optional variables with {% if %} in templates.
- Keep a manifest of variables each partial expects and validate callers against it.
When it happens
Trigger: Rendering with `Liquid::Context.new(..., strict_variables: true)` (or error_mode strict + strict_variables) while the template references a variable never assigned; variables present only in a different environment/scope than the one searched; passing raise_on_not_found: true lookups for optional keys.
Common situations: Teams enabling strict_variables to catch typos, then hitting it for legitimately optional variables; renames of assigns not propagated to all templates; partials expecting variables the caller never passes; tests with incomplete fixtures.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- unsafe parse_expression cannot be used in strict2 mode
- Bare bracket access is not allowed. Use #
- errors.syntax.for_invalid_in
- errors.syntax.for_invalid_attribute
- undefined variable #
AI-assisted analysis of Shopify/liquid@807d45a6b3 (2026-09-08).
Data as JSON: /api/errors/d742be60d3a80f2e.
Report an issue: GitHub.
Appendix: source
Thrown at lib/liquid/context.rb:236
end
# `self` resolves to a SelfDrop (enabling `self['var']` lookups),
# but only after the normal environment lookup doesn't find a value.
return @self_drop ||= SelfDrop.new(self) if fallback_to_self_drop && variable.nil?
# update variable's context before invoking #to_liquid
variable.context = self if variable.respond_to?(:context=)
liquid_variable = variable.to_liquid
liquid_variable.context = self if variable != liquid_variable && liquid_variable.respond_to?(:context=)
liquid_variable
end
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
value = obj[key]
if value.is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = value.arity == 0 ? value.call : value.call(self)
else
value
end
end
def with_disabled_tags(tag_names)
tag_names.each do |name|
@disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
end
yield
ensure
tag_names.each do |name|View on GitHub (pinned to 807d45a6b3)