Shopify/liquid · error · Liquid::ContextError

ContextError (no message)

Error message

ContextError (no message)

What it means

Liquid raises ContextError when Context#pop is called but only the single base scope remains on the scope stack — there is nothing to pop. pop is a low-level API; the comment says to use Context#stack instead, which guarantees balanced push/pop. Unbalanced calls to push (without a block) and pop trigger it.

Solutions

  1. Use `context.stack { ... }` instead of manual push/pop — it restores the stack automatically even on exceptions.
  2. Balance every manual push with exactly one pop (use ensure/begin-end).
  3. Remove any pop call made inside a Context#stack block.
  4. Refactor custom tags to use new_scope or stack helpers rather than raw scope manipulation.

Example fix

// before
context.push
render_part(context)
context.pop
// after
context.stack do
  render_part(context)
end
Defensive patterns

Strategy: try-catch

Validate before calling

# never call pop unless you pushed:
# track depth yourself
def safe_pop(ctx)
  ctx.pop if ctx.instance_variable_get(:@scopes).size > 1 rescue nil
end

Try / catch

begin
  context.pop
rescue Liquid::ContextError
  logger.warn('context.pop on empty scope stack — use context.stack instead')
end

Prevention

When it happens

Trigger: Calling context.pop without a prior context.push; calling pop twice for one push; calling pop inside Context#stack's block (stack already pops on exit); custom tags manipulating scopes manually and mis-balancing them.

Common situations: Custom tag authors writing direct scope-stack manipulation; rescuing from mid-block errors leaving pushes unbalanced; migrating old code that used the deprecated push/pop API; plugins written against very old Liquid internals.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at lib/liquid/context.rb:125

    def invoke(method, *args)
      strainer.invoke(method, *args).to_liquid
    end

    # Push new local scope on the stack. use <tt>Context#stack</tt> instead
    def push(new_scope = {})
      @scopes.unshift(new_scope)
      check_overflow
    end

    # Merge a hash of variables in the current local scope
    def merge(new_scopes)
      @scopes[0].merge!(new_scopes)
    end

    # Pop from the stack. use <tt>Context#stack</tt> instead
    def pop
      raise ContextError if @scopes.size == 1
      @scopes.shift
    end

    # Pushes a new local scope on the stack, pops it at the end of the block
    #
    # Example:
    #   context.stack do
    #      context['var'] = 'hi'
    #   end
    #
    #   context['var']  #=> nil
    def stack(new_scope = {})
      push(new_scope)
      yield
    ensure
      pop
    end

View on GitHub (pinned to 807d45a6b3)