Templates, Layouts, and the Rendering Pipeline Beginner 8 min read

Error Pages and Fallbacks

Learn how to create helpful 404 pages, password pages, and graceful fallbacks that guide users back to useful content.

Error pages are inevitable. Products get discontinued, URLs get mistyped, and pages get deleted. A well-designed error page turns a frustrating dead end into an opportunity to guide visitors back to your store’s content.

The 404 Template

The 404.json template renders when a visitor reaches a URL that doesn’t exist:

templates/404.json
{
"sections": {
"main": {
"type": "main-404",
"settings": {
"heading": "Page not found",
"text": "The page you're looking for doesn't exist or has been moved."
}
}
},
"order": ["main"]
}

A Basic 404 Section

{# sections/main-404.liquid #}
<div class="page-404">
<h1>{{ section.settings.heading }}</h1>
<p>{{ section.settings.text }}</p>
<a href="{{ routes.root_url }}" class="button">
Return to homepage
</a>
</div>
{% schema %}
{
"name": "404 Page",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Page not found"
},
{
"type": "textarea",
"id": "text",
"label": "Text",
"default": "The page you're looking for doesn't exist."
}
]
}
{% endschema %}

Making 404 Pages Helpful

A good 404 page does more than apologize. It helps visitors find what they need.

<div class="page-404">
<h1>{{ section.settings.heading }}</h1>
<p>{{ section.settings.text }}</p>
<div class="search-form-wrapper">
<p>Try searching for what you're looking for:</p>
<form action="{{ routes.search_url }}" method="get">
<input
type="search"
name="q"
placeholder="Search our store..."
aria-label="Search"
>
<button type="submit">Search</button>
</form>
</div>
<a href="{{ routes.root_url }}" class="button">
Return to homepage
</a>
</div>
<div class="page-404">
<h1>{{ section.settings.heading }}</h1>
<p>{{ section.settings.text }}</p>
{%- if section.settings.show_collections -%}
<div class="popular-collections">
<h2>Browse our collections</h2>
<ul>
{%- for collection in collections limit: 6 -%}
{%- if collection.all_products_count > 0 -%}
<li>
<a href="{{ collection.url }}">{{ collection.title }}</a>
</li>
{%- endif -%}
{%- endfor -%}
</ul>
</div>
{%- endif -%}
</div>
{% schema %}
{
"name": "404 Page",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Page not found"
},
{
"type": "textarea",
"id": "text",
"label": "Text"
},
{
"type": "checkbox",
"id": "show_collections",
"label": "Show popular collections",
"default": true
}
]
}
{% endschema %}
{%- if section.settings.collection != blank -%}
<div class="featured-on-404">
<h2>{{ section.settings.featured_heading }}</h2>
<div class="product-grid">
{%- for product in section.settings.collection.products limit: 4 -%}
{% render 'product-card', product: product %}
{%- endfor -%}
</div>
</div>
{%- endif -%}
{% schema %}
{
"name": "404 Page",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Page not found"
},
{
"type": "collection",
"id": "collection",
"label": "Featured collection"
},
{
"type": "text",
"id": "featured_heading",
"label": "Featured section heading",
"default": "You might like these"
}
]
}
{% endschema %}

The Password Page

When a store is password-protected (common before launch), visitors see the password page. This uses its own layout and template.

Password Layout

{# layout/password.liquid #}
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ shop.name }}</title>
{{ content_for_header }}
{{ 'password.css' | asset_url | stylesheet_tag }}
</head>
<body class="password-page">
{{ content_for_layout }}
</body>
</html>

Password Template

templates/password.json
{
"sections": {
"main": {
"type": "main-password",
"settings": {}
}
},
"order": ["main"]
}

Password Section

{# sections/main-password.liquid #}
<div class="password-container">
<header class="password-header">
{%- if section.settings.logo != blank -%}
<img
src="{{ section.settings.logo | image_url: width: 200 }}"
alt="{{ shop.name }}"
class="password-logo"
>
{%- else -%}
<h1>{{ shop.name }}</h1>
{%- endif -%}
</header>
<main class="password-main">
<h2>{{ section.settings.heading }}</h2>
<p>{{ section.settings.text }}</p>
{%- form 'storefront_password' -%}
<div class="password-form">
<label for="password" class="visually-hidden">Password</label>
<input
type="password"
name="password"
id="password"
placeholder="Enter password"
aria-describedby="password-error"
>
<button type="submit">Enter</button>
</div>
{%- if form.errors -%}
<p id="password-error" class="error">
{{ form.errors.messages.password }}
</p>
{%- endif -%}
{%- endform -%}
{%- if section.settings.show_newsletter -%}
<div class="password-newsletter">
<p>{{ section.settings.newsletter_text }}</p>
{% render 'newsletter-form' %}
</div>
{%- endif -%}
</main>
<footer class="password-footer">
<p>
Store owner?
<a href="/admin">Log in here</a>
</p>
</footer>
</div>
{% schema %}
{
"name": "Password Page",
"settings": [
{
"type": "image_picker",
"id": "logo",
"label": "Logo"
},
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Opening soon"
},
{
"type": "richtext",
"id": "text",
"label": "Text",
"default": "<p>Our store is getting ready. Enter the password to access.</p>"
},
{
"type": "checkbox",
"id": "show_newsletter",
"label": "Show newsletter signup",
"default": true
},
{
"type": "text",
"id": "newsletter_text",
"label": "Newsletter text",
"default": "Be the first to know when we launch:"
}
]
}
{% endschema %}

Graceful Fallbacks in Templates

Beyond dedicated error pages, build fallbacks into your regular templates.

Empty Collection

{%- if collection.products.size > 0 -%}
<div class="product-grid">
{%- for product in collection.products -%}
{% render 'product-card', product: product %}
{%- endfor -%}
</div>
{%- else -%}
<div class="collection-empty">
<h2>No products found</h2>
<p>This collection doesn't have any products yet.</p>
<a href="{{ routes.all_products_url }}" class="button">
Browse all products
</a>
</div>
{%- endif -%}

Empty Search Results

{%- if search.performed -%}
{%- if search.results_count > 0 -%}
<p>{{ search.results_count }} results for "{{ search.terms }}"</p>
{%- for result in search.results -%}
{% render 'search-result', result: result %}
{%- endfor -%}
{%- else -%}
<div class="search-no-results">
<h2>No results found</h2>
<p>We couldn't find anything for "{{ search.terms }}"</p>
<h3>Search tips:</h3>
<ul>
<li>Check your spelling</li>
<li>Try more general terms</li>
<li>Try different keywords</li>
</ul>
<h3>Popular searches:</h3>
<ul>
{%- for collection in collections limit: 4 -%}
<li>
<a href="{{ routes.search_url }}?q={{ collection.title | url_encode }}">
{{ collection.title }}
</a>
</li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}
{%- endif -%}

Empty Cart

{%- if cart.item_count > 0 -%}
{# Cart contents #}
{%- else -%}
<div class="cart-empty">
<h2>Your cart is empty</h2>
<p>Looks like you haven't added anything yet.</p>
<a href="{{ routes.all_products_url }}" class="button">
Start shopping
</a>
{%- if section.settings.empty_cart_collection != blank -%}
<div class="cart-empty-suggestions">
<h3>Popular items</h3>
<div class="product-grid">
{%- for product in section.settings.empty_cart_collection.products limit: 4 -%}
{% render 'product-card', product: product %}
{%- endfor -%}
</div>
</div>
{%- endif -%}
</div>
{%- endif -%}

Missing Images

{%- if product.featured_image -%}
<img
src="{{ product.featured_image | image_url: width: 400 }}"
alt="{{ product.featured_image.alt | escape }}"
>
{%- else -%}
<div class="placeholder-image">
{{ 'product-1' | placeholder_svg_tag: 'placeholder-svg' }}
</div>
{%- endif -%}

Shopify provides placeholder SVGs for design mode:

{{ 'collection-1' | placeholder_svg_tag }}
{{ 'product-1' | placeholder_svg_tag }}
{{ 'image' | placeholder_svg_tag }}

Redirects vs 404 Pages

Sometimes a redirect is better than a 404:

When to Use Redirects

  • Product renamed (old handle → new handle)
  • Page moved to new URL
  • Collection restructured
  • Seasonal pages that should point to new version

Redirects are managed in Shopify Admin under Online Store → Navigation → URL Redirects.

When 404 is Appropriate

  • Content permanently removed
  • Mistyped URLs
  • Old promotional pages with no replacement
  • External links to non-existent pages

Testing Error Pages

Test Your 404 Page

Visit a URL that doesn’t exist:

https://your-store.myshopify.com/this-page-does-not-exist

Test Password Page

  1. Go to Online Store → Preferences
  2. Enable password protection
  3. View your store as a visitor

Test Empty States

  • Create an empty collection
  • Search for gibberish
  • Empty your cart

Practice Exercise

Create a comprehensive 404 page that includes:

  1. A friendly error message
  2. A search form
  3. Links to main collections
  4. Featured products
{# sections/main-404.liquid #}
<div class="page-404">
<div class="page-404__content">
<h1>{{ section.settings.heading }}</h1>
<p class="page-404__message">{{ section.settings.text }}</p>
<form action="{{ routes.search_url }}" method="get" class="page-404__search">
<label for="search-404" class="visually-hidden">Search</label>
<input
type="search"
id="search-404"
name="q"
placeholder="Search our store..."
>
<button type="submit">Search</button>
</form>
<a href="{{ routes.root_url }}" class="button button--primary">
Go to homepage
</a>
</div>
{%- if section.settings.show_collections -%}
<div class="page-404__collections">
<h2>Browse collections</h2>
<ul class="collection-list">
{%- for collection in collections limit: 6 -%}
{%- if collection.all_products_count > 0 -%}
<li>
<a href="{{ collection.url }}">
{%- if collection.image -%}
<img src="{{ collection.image | image_url: width: 100 }}" alt="">
{%- endif -%}
<span>{{ collection.title }}</span>
</a>
</li>
{%- endif -%}
{%- endfor -%}
</ul>
</div>
{%- endif -%}
{%- if section.settings.featured_collection != blank -%}
<div class="page-404__featured">
<h2>{{ section.settings.featured_heading }}</h2>
<div class="product-grid product-grid--4">
{%- for product in section.settings.featured_collection.products limit: 4 -%}
{% render 'product-card', product: product %}
{%- endfor -%}
</div>
</div>
{%- endif -%}
</div>
{% schema %}
{
"name": "404 Page",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Page not found"
},
{
"type": "textarea",
"id": "text",
"label": "Message",
"default": "Sorry, the page you're looking for doesn't exist or has been moved."
},
{
"type": "checkbox",
"id": "show_collections",
"label": "Show collections",
"default": true
},
{
"type": "collection",
"id": "featured_collection",
"label": "Featured collection"
},
{
"type": "text",
"id": "featured_heading",
"label": "Featured heading",
"default": "Popular products"
}
]
}
{% endschema %}

Key Takeaways

  1. 404 pages should help visitors, not just apologize
  2. Include search on error pages to help visitors find content
  3. Show collections or products to keep visitors engaged
  4. Password pages use their own layout (password.liquid)
  5. Build fallbacks into all templates for empty states
  6. Use placeholders in design mode for empty content
  7. Prefer redirects when content has moved, 404 when removed

What’s Next?

Congratulations! You’ve completed Module 4: Templates, Layouts, and the Rendering Pipeline. You now understand how Shopify assembles pages from layouts, templates, and sections.

In the next module, you’ll dive deep into Sections, Blocks, and Schema Design to build powerful, customizable theme components.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...