Liquid Error: Nothing Renders and There Is No Error Message
Liquid fails quietly by design. Here is where the error actually goes and how to make your theme tell you what went wrong.
Disclosure: I spent four years building Shopify themes at Debutify, finishing as CDO.
The short answer
A section is blank, a value does not print, and the browser console is clean. Liquid errors do not throw, they render as nothing or as an HTML comment.
Look in the page source, not the console:
curl -s https://yourstore.com/products/example | grep -i "liquid error"
<!-- Liquid error (sections/product-form line 42): divided by 0 -->
If that returns nothing, you do not have a Liquid error, you have a value that is empty or a condition that is false. Dump the object:
<pre>{{ product | json }}</pre>
Tested against Shopify Online Store 2.0, 2026.
Why Liquid fails silently
Liquid is a template language designed to run untrusted merchant input on Shopify's servers. Throwing on error would mean a merchant's typo takes down their storefront during a sale.
So the design choice is: render what you can, skip what you cannot, never crash. An undefined variable renders as an empty string. A filter applied to the wrong type usually returns nil. A syntax error in one tag produces an HTML comment and the rest of the page continues.
That is the right trade for a hosted platform and it makes debugging genuinely harder, because the failure produces no signal anywhere you normally look.
The three ways Liquid fails
1. A real Liquid error, which leaves a comment
<!-- Liquid error (snippets/price line 8): divided by 0 -->
<!-- Liquid error (sections/header line 23): Unknown filter 'moneyy' -->
These appear in the HTML source at the point where the error happened. They are invisible in the rendered page and invisible in DevTools' Elements panel if you are not looking for comments.
View source, or curl and grep. This is the first thing to check and most people never do.
Common causes: a typo in a filter name, dividing by a value that is zero or nil, calling a filter on the wrong type, and malformed tag syntax.
2. A value that is empty
No error, because nothing went wrong. The variable simply has no value.
{{ product.metafields.custom.badge }} {%- comment -%} renders nothing {%- endcomment -%}
This is by far the most common case and it has three usual causes: the namespace or key is wrong, the metafield has no value for this product, or you are outputting the object instead of .value.
3. A condition that is false
The section renders and the branch you expected did not run.
{%- if product.metafields.custom.features -%}
A metafield object exists even when its value is empty, so this is truthy and the branch runs with nothing inside it. Conversely:
{%- if collection.products.size > 50 -%}
An unpaginated product loop caps at 50, so this is never true no matter how large the collection is.
The diagnostic sequence
1. View source and grep for Liquid error. Ten seconds, and it either gives you the answer or rules out a whole category.
2. Dump the object.
<pre style="max-height:400px;overflow:auto">{{ product | json }}</pre>
Works on product, collection, cart, customer, section.settings, block.settings, template, and shop. Seeing the actual structure resolves most "why is this empty" questions immediately.
section.settings is the one I reach for most. A setting id in the schema that does not match the id being read in the template is a silent failure and completely invisible until you dump it.
3. Print to the console instead of the page when the dump breaks your layout:
<script>console.log("settings", {{ section.settings | json }});</script>
4. Check the type.
{{ my_var.class }} {%- comment -%} not available in Shopify Liquid {%- endcomment -%}
{{ my_var | json }} {%- comment -%} use this instead {%- endcomment -%}
| json tells you whether you have a string, a number, an array, or an object. A surprising number of Liquid bugs are a value being a string when you assumed a number, because "10" > 5 does not do what you want.
5. Gate the debug output so you can leave it in:
{%- if request.design_mode -%}
<pre>{{ product | json }}</pre>
{%- endif -%}
request.design_mode is true only in the theme editor.
The specific traps
Whitespace and nil in comparisons
{%- if product.metafields.custom.note != blank -%}
blank covers nil, empty string, and empty array. empty is narrower. Comparing against "" misses nil. Use != blank for content checks, which is almost always what you want.
Filters that return nil
{{ product.price | money_without_currency | times: 2 }}
money_without_currency returns a formatted string like 1,299.00. times on a string with a comma produces nil, and the whole expression renders as empty.
Order matters: do arithmetic on the raw integer, format last.
{{ product.price | times: 2 | money }}
Shopify prices are integers in the shop's smallest currency unit, so 1299 is 12.99. Formatting first and calculating second is a very common mistake.
The 50 item loop cap
{%- for product in collection.products -%}
Caps at 50 with no warning. Your 200 product collection shows 50 and looks fine, and nobody notices for months.
{%- paginate collection.products by 24 -%}
{%- for product in collection.products -%}
...
{%- endfor -%}
{{ paginate | default_pagination }}
{%- endpaginate -%}
The paginate tag must wrap the loop, not sit beside it.
all_products lookup limits
{%- assign p = all_products[handle] -%}
There is a per template cap on these lookups. Past it, they silently return nothing, so the last few items in a hardcoded list just do not render. Use a collection instead, which is one fetch and merchant editable.
assign scope in loops and snippets
{%- assign total = 0 -%}
{%- for item in cart.items -%}
{%- assign total = total | plus: item.line_price -%}
{%- endfor -%}
{{ total }} {%- comment -%} works {%- endcomment -%}
assign inside a for does persist after the loop in Shopify Liquid. But variables assigned inside a {% render %} do not leak back to the caller, which is different from the deprecated {% include %}. Code migrated from include to render breaks silently here, and it is one of the harder ones to spot because the snippet works fine in isolation.
Pass values in and return them through the rendered output, or restructure.
Making failures visible
The theme cannot throw, and you can make it complain.
{%- comment -%} snippets/require.liquid {%- endcomment -%}
{%- unless value != blank -%}
{%- if request.design_mode -%}
<div style="background:#fee;border:2px solid #c00;padding:8px;font:12px monospace">
Missing required value: {{ label }}
</div>
{%- endif -%}
{%- endunless -%}
{%- render 'require', value: product.metafields.custom.spec, label: 'product.metafields.custom.spec' -%}
A merchant editing in the theme editor sees a red box saying exactly what is missing. Customers see nothing. That is the closest thing Liquid has to an assertion, and it turns a silent empty region into an error message somebody can act on.
Prevention
Keep the theme in git. shopify theme pull plus version control means every change is a diff and you can bisect a regression instead of guessing.
Use shopify theme dev rather than the browser code editor. Hot reload against real data changes the iteration speed enough to change how you debug.
Check Server-Timing after changes so a performance regression is caught immediately:
curl -sI https://yourstore.com/collections/all | grep -i server-timing
Test with a realistic catalogue. A development store with 12 products will never show you the 50 item cap or the reference resolution cost. Ask for the merchant's catalogue size and generate something comparable.
Grep for Liquid error in CI if you have a staging store. A simple curl over your main templates, failing the build if the string appears, catches the loud category automatically.