5 min read
On this page

Layouts & Error Handling

Overview

Layouts and error boundaries are the structural backbone of a SvelteKit application. Layouts wrap child pages with shared UI like navigation, sidebars, and footers. Error boundaries catch failures and display fallback content instead of crashing the page. Together, they let you build applications where every page has consistent structure and every failure is handled gracefully.

+layout.svelte Wraps Child Pages

A +layout.svelte file wraps every +page.svelte in the same directory and all subdirectories. The layout receives its children as a snippet and decides where to render them.

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { type Snippet } from 'svelte';

  let { children }: { children: Snippet } = $props();
</script>

<header>
  <nav>
    <a href="/">Home</a>
    <a href="/products">Products</a>
    <a href="/account">Account</a>
  </nav>
</header>

<main>
  {@render children()}
</main>

<footer>
  <p>2026 Our Company</p>
</footer>

Every page in the application renders inside this structure. The nav, main wrapper, and footer are always present. Only the content inside {@render children()} changes when the user navigates.

Nested Layouts Compose

Layouts stack. A child layout renders inside its parent layout, and its own children render inside it.

src/routes/
  +layout.svelte              → root layout (nav + footer)
  shop/
    +layout.svelte            → shop layout (category sidebar)
    +page.svelte              → /shop
    [category]/
      +layout.svelte          → category layout (filters)
      +page.svelte            → /shop/electronics
      [product]/
        +page.svelte          → /shop/electronics/headphones
<!-- src/routes/shop/+layout.svelte -->
<script lang="ts">
  import { type Snippet } from 'svelte';

  let { children }: { children: Snippet } = $props();
</script>

<div class="shop-layout">
  <aside class="category-nav">
    <a href="/shop/electronics">Electronics</a>
    <a href="/shop/clothing">Clothing</a>
    <a href="/shop/books">Books</a>
  </aside>
  <div class="shop-content">
    {@render children()}
  </div>
</div>
<!-- src/routes/shop/[category]/+layout.svelte -->
<script lang="ts">
  import { type Snippet } from 'svelte';

  let { data, children }: { data: any; children: Snippet } = $props();
</script>

<div class="category-layout">
  <div class="filters">
    <h3>Filter by</h3>
    {#each data.filters as filter}
      <label>
        <input type="checkbox" value={filter.id} />
        {filter.name}
      </label>
    {/each}
  </div>
  <div class="products">
    {@render children()}
  </div>
</div>

When visiting /shop/electronics/headphones, the rendering order is: root layout, then shop layout, then category layout, then the product page. Each layout adds one layer of UI.

+layout.server.ts for Layout Data

Layouts can load data the same way pages do. A +layout.server.ts file exports a load function whose data is available to the layout and all its child pages.

// src/routes/shop/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async () => {
  const categories = await db.category.findMany({
    orderBy: { name: 'asc' }
  });
  return { categories };
};
// src/routes/shop/[category]/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ params }) => {
  const filters = await db.filter.findMany({
    where: { categorySlug: params.category }
  });
  return { filters };
};

Layout data is inherited. A page at /shop/electronics can access both data.categories from the shop layout and data.filters from the category layout, because parent() data merges down.

// src/routes/shop/[category]/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params, parent }) => {
  const layoutData = await parent();
  // layoutData.categories is available from the shop layout
  // layoutData.filters is available from the category layout

  const products = await db.product.findMany({
    where: { categorySlug: params.category }
  });

  return { products };
};

Error Boundaries with +error.svelte

When a load function throws or calls error(), SvelteKit walks up the layout tree looking for the nearest +error.svelte file. This is the error boundary.

<!-- src/routes/+error.svelte -->
<script lang="ts">
  import { page } from '$app/stores';
</script>

<div class="error-page">
  <h1>{$page.status}</h1>
  <p>{$page.error?.message}</p>
  <a href="/">Return to homepage</a>
</div>

Error boundaries are scoped. An error in a child route renders the nearest ancestor error boundary, while layouts above that boundary stay intact.

src/routes/
  +layout.svelte          → stays rendered (nav is visible)
  +error.svelte           → fallback for top-level errors
  shop/
    +layout.svelte        → stays rendered if error is deeper
    +error.svelte         → catches errors in /shop/* routes
    [category]/
      +page.svelte        → if this page's load fails...

If the category page's load function throws, the shop error boundary renders inside the shop layout. The root layout with its navigation stays on screen. The user sees the nav, the shop sidebar, and an error message where the product listing would be.

<!-- src/routes/shop/+error.svelte -->
<script lang="ts">
  import { page } from '$app/stores';
</script>

<div class="shop-error">
  <h2>Something went wrong</h2>
  <p>{$page.error?.message}</p>
  <a href="/shop">Browse all categories</a>
</div>

The Root Layout

The root layout at src/routes/+layout.svelte is special. It wraps every page in the application, including error pages. If you do not create one, SvelteKit uses a minimal default that just renders children.

The root layout is the right place for global concerns.

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { type Snippet } from 'svelte';
  import '../app.css';

  let { children }: { children: Snippet } = $props();
</script>

<svelte:head>
  <meta name="description" content="Our application" />
</svelte:head>

{@render children()}

The root layout cannot have an +error.svelte sibling that catches its own errors. If the root layout's load function fails, SvelteKit renders a static fallback error page. Keep root layout load functions minimal and unlikely to fail.

Resetting Layouts with @

Sometimes a page needs to break out of the layout stack. A login page should not show the dashboard sidebar. A print-friendly page should not have navigation. The @ syntax resets the layout chain.

src/routes/
  +layout.svelte              → root layout
  dashboard/
    +layout.svelte            → dashboard layout (sidebar)
    +page.svelte              → /dashboard (uses dashboard layout)
    settings/
      +page.svelte            → /dashboard/settings (uses dashboard layout)
    print/
      +page@.svelte           → /dashboard/print (uses ONLY root layout)

The @ followed by nothing resets to the root layout. You can also reset to a specific layout by naming it.

src/routes/
  (app)/
    +layout.svelte            → app layout
    dashboard/
      +layout.svelte          → dashboard layout
      report/
        +page@(app).svelte    → uses app layout, skips dashboard layout
<!-- src/routes/dashboard/print/+page@.svelte -->
<script lang="ts">
  let { data } = $props();
</script>

<div class="print-layout">
  <h1>Dashboard Report</h1>
  <table>
    {#each data.metrics as metric}
      <tr>
        <td>{metric.name}</td>
        <td>{metric.value}</td>
      </tr>
    {/each}
  </table>
</div>

This page renders inside the root layout only. No dashboard sidebar, no navigation clutter. Just the content.

404 Handling

SvelteKit handles 404 errors automatically. When no route matches a URL, SvelteKit renders the nearest +error.svelte with a 404 status.

For dynamic routes, you throw 404 errors explicitly when a resource is not found.

// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';

export const load: PageServerLoad = async ({ params }) => {
  const post = await db.post.findUnique({
    where: { slug: params.slug }
  });

  if (!post) {
    error(404, {
      message: 'Post not found',
      code: 'POST_NOT_FOUND'
    });
  }

  return { post };
};
<!-- src/routes/blog/+error.svelte -->
<script lang="ts">
  import { page } from '$app/stores';
</script>

{#if $page.status === 404}
  <div class="not-found">
    <h2>Post not found</h2>
    <p>The blog post you are looking for does not exist or has been removed.</p>
    <a href="/blog">View all posts</a>
  </div>
{:else}
  <div class="error">
    <h2>Error {$page.status}</h2>
    <p>{$page.error?.message}</p>
  </div>
{/if}

You can also create a catch-all route for custom 404 pages that need special behavior.

src/routes/
  [...path]/
    +page.svelte    → custom 404 page for any unmatched URL
// src/routes/[...path]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
  error(404, { message: 'Page not found' });
};

Common Pitfalls

  • Root layout errors are unrecoverable: If the root layout's load function throws, there is no error boundary to catch it. SvelteKit shows a plain error page. Keep root layout data loading simple and fault-tolerant.
  • Layout data waterfalls: Each layout load function runs after its parent's load function completes (when using parent()). Avoid deeply nested layouts that each fetch data, as this creates a chain of sequential network requests.
  • Forgetting that layouts persist across navigation: When navigating between sibling pages, the shared layout does not unmount. State in the layout component persists. This can cause stale data if you store local state in a layout instead of deriving it from loaded data.
  • Over-nesting layouts: Not every directory needs a layout. Adding layouts that just pass through children adds complexity without benefit. Only create a layout when there is genuinely shared UI.
  • Misunderstanding @ reset scope: +page@.svelte resets to the root layout. +page@(group).svelte resets to the named group's layout. Getting the target wrong means your page either has too much or too little surrounding UI.

Key Takeaways

  • +layout.svelte wraps all pages in its directory and subdirectories. Nested layouts compose automatically, each adding one layer of UI.
  • +layout.server.ts loads data for layouts. Child pages can access ancestor layout data through parent().
  • +error.svelte creates error boundaries. Errors bubble up to the nearest error boundary, keeping parent layouts visible.
  • The root layout wraps everything, including error pages. Keep its load function minimal because root errors have no boundary to catch them.
  • The @ syntax resets the layout chain. Use it for pages like login, print views, or landing pages that need a different structure than their siblings.
  • 404 handling is automatic for unmatched routes and explicit for missing resources in dynamic routes. Use error(404, ...) in load functions when a database lookup returns nothing.