# Theme debugging

Source: https://support.storeconnect.com/articles/theme-debugging · Last modified 21 August 2026

StoreConnect provides two template-level debugging tags — `{% debug %}` and `{% timer %}` — whose output appears in the **Console**, a real-time debugging panel connected to the storefront via WebSocket. The Console shows debug lines, timers, errors, warnings, and the full template rendering tree for each request.

## The `{% debug %}` tag

Logs key-value pairs to the Console. Use it to inspect variables, check conditions, and trace template execution.


```liquid

{% debug product_name: current_product.name, product_id: current_product.id %}
```


The Console shows:

```

product_name: Widget Pro
product_id: a1B2c3D4e5F6g7H8
```

`{% debug %}` does **not** output anything to the page — it is safe to leave in templates during development.

### Common debugging patterns

**Check if a variable exists:**


```liquid

{% debug
  has_product: current_product != blank,
  has_customer: current_customer != blank,
  cart_empty: current_cart.empty?
%}
```


**Inspect drop properties:**


```liquid

{% debug
  product_name: current_product.name,
  product_price: current_product.price,
  product_available: current_product.available,
  variant_count: current_product.variants.size
%}
```


**Trace execution path:**


```liquid

{% debug step: "before variant check", variant_types: current_product.variant_types.size %}
{% if current_product.variant_types.size > 0 %}
  {% debug step: "has variants", first_type: current_product.variant_types.first.name %}
{% else %}
  {% debug step: "no variants" %}
{% endif %}
```


**Inspect request data:**


```liquid

{% debug
  path: current_request.path,
  params_q: current_request.params.q,
  params_page: current_request.params.page,
  method: current_request.method
%}
```


**Inspect form state:**


```liquid

{% form "add-to-cart", product_id: current_product.id %}
  {% debug
    form_errors_count: form.errors.size,
    quantity_name: form.quantity.name,
    quantity_value: form.quantity.value
  %}
{% endform %}
```


**Inspect search or filter data:**


```liquid

{% debug
  search_type: current_search.type,
  search_total: current_search.total,
  field_count: current_search.fields.size
%}
{% for field in current_search.fields %}
  {% debug field_name: field.name, option_count: field.options.size %}
{% endfor %}
```


**Check custom data fields:**


```liquid

{% debug
  custom_tier: current_customer.data.loyalty_tier,
  custom_notes: current_order.data.special_instructions
%}
```


### Inspect available fields on a drop

Use `record_fields` and `record_relationships` to discover what properties are available on a drop. Filters cannot be used directly inside `{% debug %}` tag parameters, so assign them first:


```liquid

{%- assign product_fields = current_product | record_fields | join: ", " -%}
{%- assign product_rels = current_product | record_relationships | join: ", " -%}
{%- assign product_type = current_product | record_name -%}
{% debug product_fields: product_fields, product_rels: product_rels, type: product_type %}
```


### Dump an object as JSON


```liquid

{%- assign cart_json = current_cart | json -%}
{%- assign fields_json = current_search.fields | json -%}
{% debug cart_json: cart_json, search_fields: fields_json %}
```


### Conditional debug output to the page

If you do not have Console access, output debug info directly to the page (remove before production):


```liquid

{% if current_request.params.debug == "true" %}
  <pre style="background:#111;color:#0f0;padding:1em;font-size:12px;overflow:auto">
    product: {{ current_product | json }}
    cart items: {{ current_cart.item_count }}
    customer: {{ current_customer.name | default: "not logged in" }}
  </pre>
{% endif %}
```


Visit the page with `?debug=true` to see the output.

## The `{% timer %}` tag

Wraps a block of template code and measures execution time in milliseconds. Results appear in the Console's timer section.


```liquid

{% timer "product_render" %}
  {% render "products/product/images", product: current_product %}
  {% render "products/product/details", product: current_product %}
{% endtimer %}
```


Console shows:

```

product_render: 45.2ms
```

### Performance debugging patterns

**Identify the slowest section of a page:**


```liquid

{% timer "header" %}
  {% render "header" %}
{% endtimer %}

{% timer "main_content" %}
  {{ body_content }}
{% endtimer %}

{% timer "footer" %}
  {% render "footer" %}
{% endtimer %}
```


**Narrow down within a slow section:**


```liquid

{% timer "product_images" %}
  {% render "products/product/images", product: current_product %}
{% endtimer %}

{% timer "product_variants" %}
  {% render "products/product/variant_selector", product: current_product %}
{% endtimer %}

{% timer "related_products" %}
  {% render "products/related_products", product: current_product %}
{% endtimer %}
```


**Measure cache effectiveness:**


```liquid

{% timer "product_uncached" %}
  {% render "products/product/full", product: current_product %}
{% endtimer %}

{% timer "product_cached" %}
  {% cache "product", items: [current_product, current_store] %}
    {% render "products/product/full", product: current_product %}
  {% endcache %}
{% endtimer %}
```


:::note
Timer handles are static strings, not dynamic expressions. Each iteration of a loop reports under the same handle — the Console shows cumulative time, which is useful for identifying whether a loop is the bottleneck.
:::

## Debugging steps

1. **Reproduce the issue** — identify the exact page, URL, and conditions.
2. **Add `{% debug %}` tags** to inspect relevant variables near the problem area.
3. **Check the Console** for debug output, errors, and warnings.
4. **If it is a performance issue**, wrap sections in `{% timer %}` to find the slow part.
5. **Narrow down** — move debug or timer tags closer to the root cause.
6. **Check the data** — use `record_fields` to verify what properties exist on a drop.
7. **Fix the issue** and remove debug/timer tags (or leave them if useful for ongoing monitoring).

## Common gotchas

### Multi-store URL routing

StoreConnect supports multiple stores under a single installation. When stores share a domain, every URL is prefixed with the store path:

```

https://example.com/store-a/products
https://example.com/store-b/cart
```

Always use URL helpers or the `params` filter rather than hardcoding paths. Hardcoded paths break in multi-store setups.

### Template resolution order

Templates resolve in this order:

1. Client theme — the store's custom theme.
2. Base theme — the default StoreConnect theme.

This means:
- Client themes only need files they want to override.
- Removing a file from the client theme restores the base theme version.
- New base theme files are automatically available unless overridden.

### Pagination

Collections must be wrapped in `{% paginate %}` to limit results:


```liquid

{% paginate all_products by 20 %}
  {% for product in all_products %}
    {{ product.name }}
  {% endfor %}
{% endpaginate %}
```


:::warning
Without `{% paginate %}`, collections may return all records, which can be very slow on stores with large catalogs.
:::

The `paginate` tag accepts:
- `by` — items per page (required)
- `as` — custom URL parameter name (default: `page`)
- `window` — number of page links to show (default: 5)

### Cache behavior

The `{% cache %}` tag caches rendered HTML fragments:


```liquid

{% cache "product", items: [current_product, current_store, current_customer, current_cart] %}
  {%- comment -%} Expensive rendering {%- endcomment -%}
{% endcache %}
```


Always include all objects that affect the output in the `items` list:
- Include `current_customer` if the output varies by logged-in user.
- Include `current_cart` if the output shows cart state.
- Use `expires_in` for time-based expiration.

### Records vs drops

The `{% query %}` tag returns Record objects, not Drops. They are different:

| | Records | Drops |
|---|---------|-------|
| Custom fields | `record.custom_data.Field__c` | `drop.data.field_name` |
| Field names | API names | Lowercase, normalized |
| Methods | Basic field access | Rich computed properties |
| Conversion | `record \| cast: "Product"` | Already a drop |

Use `cast` to convert a Record to a Drop when you need drop methods:


```liquid

{% query 'Product2' as raw_products %}
{% for record in raw_products %}
  {%- assign product = record | cast: "Product" -%}
  {{ product.price | money }}
{% endfor %}
```


### Custom data fields

All major objects support custom data fields. Access them via the `data` property on drops:


```liquid

{{ product.data.custom_field_name }}
{{ order.data.special_instructions }}
{{ current_customer.data.loyalty_tier }}
```


On Record objects (from `{% query %}`), use `custom_data` instead, with the field's API name (including the `__c` suffix) as mapped in your Custom Data Mappings configuration:


```liquid

{% query 'Product2' as products %}
{% for record in products %}
  {{ record.custom_data.Custom_Field__c }}
{% endfor %}
```


See [Add custom data fields to your store](liquid-custom-data-fields) for how to configure Custom Data Mappings and the full list of supported field types.

### Whitespace control

Liquid tags support whitespace trimming with `-`:


```liquid

{%- assign x = "hello" -%}
{%- if true -%}
  Content
{%- endif -%}
```


The `-` trims whitespace on the side it appears. Use it liberally to prevent unwanted whitespace in HTML output.

### `body_content` vs `current_page.body_content`

These are different variables:


```liquid

{%- comment -%} In layouts/theme.liquid — the fully rendered page template output {%- endcomment -%}
{{ body_content }}

{%- comment -%} In pages/home.liquid — the page's CMS content blocks {%- endcomment -%}
{{ current_page.body_content }}
```


### The `{% default %}` tag vs the `| default:` filter

These are different:


```liquid

{%- comment -%} Tag: sets variable if not already defined {%- endcomment -%}
{% default show_price: true, max_items: 10 %}

{%- comment -%} Filter: provides fallback for nil or empty values {%- endcomment -%}
{{ product.description | default: "No description" }}
```


Use the tag at the top of snippets to define default parameter values. Use the filter inline to provide fallbacks for nil or blank values.

## Differences from Shopify Liquid

While StoreConnect uses the Liquid template language, there are important differences from Shopify:

| Feature | Shopify | StoreConnect |
|---------|---------|-------------|
| Variant system | Fixed (up to 3 options) | N-ary (unlimited variant types) |
| Data access | Section/block settings | Drops and custom data fields |
| Components | Sections with JS API | Reloadable components with events |
| Controllers | None | Liquid controllers (before/after/final) |
| Forms | `{% form %}` (limited types) | `{% form %}` (30+ types) |
| Querying | Limited Liquid access | `{% query %}` tag for direct queries |
| Session storage | Not available | `{% session %}` tag |
| Structs | Not available | `{% struct %}` tag |
| Object creation | Not available | `{% new %}` tag (UUID, List, Map, Rand) |
| Custom fields | Metafields | `data.*` property on all drops |
| Themes | File-based only | File-synced to database |
| Layouts | Single layout | Multiple layouts with `{% layout %}` |
| Content blocks | Sections/blocks | CMS content blocks with types |
| Search | Limited | Full search with trait filters |

## Markdown rendering

Content fields (page body, article body, product descriptions) support Markdown rendering. The content is converted to HTML before output. Use the `sc-rich-text` CSS class on the container for proper styling:


```liquid

<div class="sc-rich-text">
  {{ current_page.body_content }}
</div>
```

---

## Follow StoreConnect

- [Email Newsletter](https://getstoreconnect.com/c/lp-newsletter)
- [LinkedIn Newsletter](https://www.linkedin.com/build-relation/newsletter-follow?entityUrn=7444956928444862464)
- [YouTube](https://www.youtube.com/channel/UCngKdP2x8l1wcbAKW3tvU8g)
- [LinkedIn](https://www.linkedin.com/company/storeconnect)
- [X / Twitter](https://x.com/storeconnecthq)

## Popular Links

- [Partners](https://getstoreconnect.com/partners)
- [News](https://getstoreconnect.com/articles/news)
- [Events](https://getstoreconnect.com/articles/events)
- [Feature Comparison](https://getstoreconnect.com/how-we-compare)
- [Download a free trial](https://appexchange.salesforce.com/appxListingDetail?listingId=a0N3A00000FMkeKUAT)
- [Book a Demo](https://getstoreconnect.com/contact)

## Documentation

- [Help documentation](https://support.storeconnect.com/help-documentation)
- [AI agents](https://support.storeconnect.com/ai)
- [Videos & tutorials](https://support.storeconnect.com/videos-tutorials)
- [Developer reference](https://support.storeconnect.com/developer-reference)
- [Release notes](https://support.storeconnect.com/release-notes)
- [Troubleshooting](https://support.storeconnect.com/troubleshooting)
- [Trust Center](https://trust.getstoreconnect.com/)
- [Status Page](https://status.storeconnect.com/)

## Contact

- info@getstoreconnect.com
- US +1 415 745 3230
- AUS +61 2 8365 2308

100 S Ashley Dr, Suite 600-2461
Tampa FL 33602-600 USA

Level 22, Sydney Place
180 George Street
Sydney, NSW, 2000, AUS

---

StoreConnect Support — https://support.storeconnect.com/articles/theme-debugging