Shopify/liquid · warning · Liquid::UndefinedVariable

undefined variable #

Error message

undefined variable #{key}

What it means

VariableLookup#evaluate raises Liquid::UndefinedVariable, "undefined variable #{key}", only when the context's strict_variables option is enabled and a lookup key resolves to nothing (and is not a supported keyword). Without strict_variables the same situation silently returns nil.

Solutions

  1. Provide the variable in the assigns Hash or registers before rendering.
  2. Fix the template to reference an existing key (or use {{ key | default: '' }}).
  3. Guard with {% if key %} or use the default filter so the lookup is only evaluated when present.
  4. If nil is a legitimate value, disable strict_variables for that render.

Example fix

// before
template.render({}, strict_variables: true) # template: {{ user_name }}
// after
template.render({ 'user_name' => 'bob' }, strict_variables: true)
Defensive patterns

Strategy: try-catch

Validate before calling

required_keys.each { |k| raise "missing template variable #{k}" unless assigns.key?(k) }

Type guard

def defined_in_context?(key, assigns)
  assigns.key?(key) || assigns.key?(key.to_s) || assigns.key?(key.to_sym)
end

Try / catch

begin
  template.render(assigns, strict_variables: true)
rescue Liquid::UndefinedVariable => e
  logger.warn("Template referenced #{e.message.sub('undefined variable ', '')}")
  template.render(assigns)
end

Prevention

When it happens

Trigger: Rendering with context strict_variables: true (often paired with strict_filters) while a template references {{ missing_key }} or {{ obj.missing_attr }} that no assign/register provides.

Common situations: Teams enabling strict_variables in CI to catch template typos; renamed variables in Ruby code not updated in templates; partials rendered with different assigns than expected.

Related errors


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

Appendix: source

Thrown at lib/liquid/variable_lookup.rb:83

          object = res.to_liquid

          # Some special cases. If the part wasn't in square brackets and
          # no key with the same name was found we interpret following calls
          # as commands and call them on the current object
        elsif lookup_command?(i) && object.respond_to?(key)
          object = object.send(key).to_liquid

        # Handle string first/last like ActiveSupport does (returns first/last character)
        # ActiveSupport returns "" for empty strings, not nil
        elsif lookup_command?(i) && object.is_a?(String) && (key == "first" || key == "last")
          object = key == "first" ? (object[0] || "") : (object[-1] || "")

          # No key was present with the desired value and it wasn't one of the directly supported
          # keywords either. The only thing we got left is to return nil or
          # raise an exception if `strict_variables` option is set to true
        else
          return nil unless context.strict_variables
          raise Liquid::UndefinedVariable, "undefined variable #{key}"
        end

        # If we are dealing with a drop here we have to
        object.context = context if object.respond_to?(:context=)
      end

      object
    end

    def ==(other)
      self.class == other.class && state == other.state
    end

    protected

    def state
      [@name, @lookups, @command_flags]
    end

View on GitHub (pinned to 807d45a6b3)