The Shopify Theme Debugging Toolkit
You cannot attach a debugger to Liquid. Here is the set of techniques that actually works for finding out why a theme is doing something unexpected.
Disclosure: I spent four years building Shopify themes at Debutify, finishing as CDO. These are the techniques my team used daily.
Debugging a Shopify theme is unusual because you have less access than you are used to. Liquid renders on Shopify's servers. There is no breakpoint, no step through, no console, no log file you can tail. You cannot attach a profiler and you cannot add instrumentation to the runtime.
What you have instead is a small set of techniques that, used well, cover most of what you need. This is the toolkit.
Print debugging, Liquid style
Since you cannot inspect state, you print it. The most useful single trick in Liquid work:
<pre>{{ product | json }}</pre>
That dumps the entire object as JSON into the page. You immediately see every available property, which is far better than guessing at the documentation, and it tells you what is actually populated for this specific product rather than what could be populated in theory.
Works on most Liquid objects:
<pre>{{ cart | json }}</pre>
<pre>{{ collection | json }}</pre>
<pre>{{ customer | json }}</pre>
<pre>{{ section.settings | json }}</pre>
<pre>{{ block.settings | json }}</pre>
section.settings is the one I reach for most when a theme editor setting is not doing what it should. Nine times out of ten the setting id in the schema does not match the id being read in the template, and dumping the object makes the typo obvious in two seconds.
Make it conditional so you can leave it in during development:
{%- if request.design_mode or shop.permanent_domain contains 'dev' -%}
<pre style="max-height:300px;overflow:auto">{{ product | json }}</pre>
{%- endif -%}
request.design_mode is true inside the theme editor, which is a convenient gate.
For something you do not want visible on the page, push it into the browser console instead:
<script>
console.log('product', {{ product | json }});
console.log('settings', {{ section.settings | json }});
</script>
Now it is in DevTools, filterable, expandable, and not disturbing the layout.
Server-Timing for render performance
Shopify returns a Server-Timing header on storefront responses:
curl -sI https://yourstore.com/products/example | grep -i server-timing
server-timing: processing;dur=180, db;dur=45, render;dur=120
processing;dur is the total server side time in milliseconds. Rough guide: under 200 is healthy, 200 to 600 is worth a look, over 600 means something in your Liquid is expensive, and over 1500 is almost always a loop touching an expensive object.
This is the single most useful number in Shopify theme work, because it cleanly separates server problems from browser problems. If processing is 120ms and the page still feels slow, stop looking at Liquid and go look at JavaScript.
Compare templates to isolate where the cost is:
for path in / /collections/all /products/example /cart; do
printf "%-22s " "$path"
curl -sI "https://yourstore.com$path" | grep -oiP 'processing;dur=\K[0-9.]+'
done
Run that before and after a change and you have a regression test for render performance.
Bisecting a template
With no profiler, bisection is how you localise an expensive or misbehaving section.
Comment out half your sections in the JSON template, measure, and repeat. Five iterations narrows a page to a single section. It is crude and it is reliable.
For a finer grained pass inside a section, comment out blocks one at a time. When you find the culprit, look for the usual expensive patterns: collection.products inside a loop, all_products lookups, metafield resolution per item, or nested loops over products.
The theme inspector
Shopify's Theme Inspector Chrome extension gives you a flame graph of Liquid render time by line. When it works it is the best tool available for this, and it is the closest thing to a real profiler that exists for Liquid.
Two caveats. It requires theme preview mode, and it only works on themes you have access to through a store you are a staff member of. It also does not always agree precisely with Server-Timing, since it measures render specifically rather than total processing, but the relative attribution is what matters and that is accurate.
Reading what actually loaded
Half of theme debugging is not about your theme at all, it is about what apps injected.
Open DevTools, Network tab, disable cache, reload, filter to JS, and sort by size. Then look at the domains. Anything not on cdn.shopify.com is a third party and belongs to an app.
For main thread cost rather than payload size, run a Performance recording and open the Bottom Up panel grouped by URL. That gives you script evaluation time per domain, which is what actually breaks interactivity.
To see what is bound to a misbehaving element, inspect it and open the Event Listeners panel. Two listeners on the same click event from two different source files is the signature of a theme and app fighting over the same button.
Preview and isolation tricks
Duplicate the theme before you debug it. Always. It takes fifteen seconds and it is the difference between an investigation and an incident.
Preview a theme by ID to test without publishing:
https://yourstore.com/?preview_theme_id=123456789
Disable JavaScript entirely in DevTools settings to see what the server rendered. If the bug is in the raw HTML, it is Liquid. If the HTML is right and the page is wrong, it is JavaScript. This one binary check saves a lot of time.
Test with apps disabled. Turn off every app embed in the theme editor and reload. If the problem goes away, bisect the embeds.
Use ?view= for alternate templates. If you have product.custom.liquid, hit /products/example?view=custom to render it without changing any product's assigned template.
The CLI is the biggest upgrade
If you are editing themes in the browser code editor, moving to the Shopify CLI is the single largest improvement available to you.
shopify theme dev --store yourstore.myshopify.com
You get a local development server with hot reload, running against real store data. Edit a file, save, and the browser updates. Compared to the browser editor's save and reload cycle this is transformative, and it means you can iterate at the speed the debugging problem actually requires.
Pair it with version control:
shopify theme pull --store yourstore.myshopify.com
git init && git add -A && git commit -m "baseline"
Now every app that edits your theme produces a visible diff. This turns "which app broke my theme" from archaeology into git diff, and it is the highest value habit in Shopify development.
Checking the storefront data directly
Sometimes the question is not what the theme is doing but what data Shopify has.
Cart state:
https://yourstore.com/cart.js
Returns the current cart as JSON. Useful when the drawer shows something different from what checkout has, which is nearly always a stale client side copy.
Product data:
https://yourstore.com/products/example.js
The product as the storefront sees it, including all variants and their availability. When a variant picker is misbehaving, compare this against what the theme rendered.
Recommendations:
https://yourstore.com/recommendations/products.json?product_id=123&limit=4
Section rendering:
https://yourstore.com/?section_id=header
Returns just that section's HTML. Extremely useful for checking whether a section renders correctly in isolation, and it is the endpoint behind the Section Rendering API.
Debugging the theme editor specifically
Settings that work on the live page but not in the editor, or vice versa, are their own category.
request.design_mode is true inside the editor. Some apps and themes deliberately behave differently there, and that is frequently the explanation for "it looks right in the editor and wrong on the site".
Sections rendered by the editor after a settings change go through the Section Rendering API, not a full page load. So any JavaScript that runs once on DOMContentLoaded will not re run when the merchant changes a setting, and the section appears broken in the editor while being fine on the live site.
The fix is to listen for the editor's events:
document.addEventListener('shopify:section:load', (e) => {
initSection(e.target);
});
document.addEventListener('shopify:section:unload', (e) => {
teardownSection(e.target);
});
Any section with JavaScript needs these. Their absence is one of the most common theme bugs I encounter, and it only ever shows up in the editor, which is why it survives testing.
Things that do not work, so you do not waste time
There is no Liquid debugger. No breakpoints, no stepping. Accept it and print.
There is no error log. A Liquid error usually renders as nothing at all, or as a Liquid error comment in the HTML source. View source and search for Liquid error, because it will not appear in the console.
{% comment %} still evaluates some things. Wrapping expensive code in a comment block does not reliably remove its cost for measurement purposes. Delete it or move it out of the template properly when bisecting.
You cannot see other merchants' app behaviour. If an app misbehaves, you are limited to the network tab and the app developer's support.
The order I actually work in
For a slow page: check Server-Timing first. If processing is high, bisect the template. If processing is fine, it is a browser problem, so go to the Network tab and look at third party JavaScript.
For a broken feature: disable JavaScript to determine whether it is Liquid or JS. If Liquid, dump the relevant object as JSON. If JavaScript, read the first console error and identify the domain.
For "it broke after installing something": duplicate the theme, disable all app embeds, confirm it is fixed, re enable one at a time.
For "it works on the site but not in the editor": check for the section load event handlers.
That decision tree covers the large majority of what comes up, and each branch takes a couple of minutes to resolve rather than an afternoon.