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
- Use `context.stack { ... }` instead of manual push/pop — it restores the stack automatically even on exceptions.
- Balance every manual push with exactly one pop (use ensure/begin-end).
- Remove any pop call made inside a Context#stack block.
- 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
- Always prefer context.stack { ... } over manual push/pop.
- Wrap manual scope changes in begin/ensure so exceptions can't unbalance the stack.
- Never pop inside a Context#stack block.
- Audit custom tags for direct @scopes manipulation when upgrading Liquid.
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
- errors.syntax.unexpected_else (locale: Unexpected 'else'…
- errors.syntax.invalid_delimiter
- errors.syntax.unknown_tag (locale: 'Unknown tag 'tag'')
- errors.syntax.tag_never_closed
- Nesting too deep
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)