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

  1. Define/assign the missing variable before use: `{% assign key = ... %}` or pass it in the render assigns hash.
  2. Pass the variable via `Template#render('key' => value)` or Environment data so lookups succeed.
  3. 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.
  4. 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

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


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)