🍡 mochi
← All demos

Error Boundaries

Each section contains a component that throws on purpose. Without boundaries, any one of them would take down this whole page; with boundaries, the failure is walled off and the rest of the page keeps working. In dev, the failed island shows a small dashed-red marker; in prod, it disappears silently.

1. Non-hydrated component + user <svelte:boundary>

Non-hydrated components don't get an automatic boundary. If they can throw during SSR, wrap them in a hand-written <svelte:boundary> yourself — otherwise the throw bubbles up and crashes the whole request.

Caught by user-written boundary: Island error

2. mochi:hydrate — SSR throw (recovery)

The framework auto-wraps every mochi:hydrate island in a boundary. This island throws on the server only — the boundary catches the SSR throw and the rest of the page renders unaffected.

3. mochi:hydrate — client throw

SSR is fine, but the script throws synchronously once the island tries to hydrate. The defensive try/catch around hydrate() catches it and swaps the island for the failure stub — the rest of the page (already rendered on the server) is untouched.

This island renders fine on the server. Once the client tries to hydrate, the script throws synchronously — the boundary catches it and swaps in the failure stub.

4. mochi:defer — server island throw

Server islands render at a separate endpoint. When that render throws, the endpoint catches it and returns a 200 with an error stub — so the browser doesn't burn its retry budget on a deterministic failure. The user-supplied loading children stay until the response arrives.

Loading from server…

5. mochi:defer — healthy island, inner mochi:hydrate client throw

The server island itself renders successfully — it's the inner mochi:hydrate child that throws once it tries to hydrate on the client. The child's auto-boundary catches its own failure, so the rest of the server island content stays intact.

Loading from server…
<script lang="ts">
  import ThrowOnSsr from './ThrowOnSsr.svelte';
  import ThrowOnClient from './ThrowOnClient.svelte';
  import ThrowOnServerIsland from './ThrowOnServerIsland.svelte';
  import HealthyServerIsland from './HealthyServerIsland.svelte';
</script>

{#snippet caughtFallback(error: unknown)}
  <div class="caught">
    Caught by user-written boundary:
    <code>{error instanceof Error ? error.message : String(error)}</code>
  </div>
{/snippet}

<section class="mode">
  <header>
    <h2>1. Non-hydrated component + user <code>&lt;svelte:boundary&gt;</code></h2>
    <p>
      Non-hydrated components don't get an automatic boundary. If they can throw during SSR, wrap them in a hand-written <code>&lt;svelte:boundary&gt;</code> yourself — otherwise the
      throw bubbles up and crashes the whole request.
    </p>
  </header>
  <svelte:boundary failed={caughtFallback}><ThrowOnSsr label="ThrowOnSsr (no directive)" /></svelte:boundary>
</section>

<section class="mode">
  <header>
    <h2>2. <code>mochi:hydrate</code> — SSR throw (recovery)</h2>
    <p>
      The framework auto-wraps every <code>mochi:hydrate</code> island in a boundary. This island throws on the server only — the boundary catches the SSR throw and the rest of the
      page renders unaffected.
    </p>
  </header>
  <ThrowOnSsr mochi:hydrate label="ThrowOnSsr" />
</section>

<section class="mode">
  <header>
    <h2>3. <code>mochi:hydrate</code> — client throw</h2>
    <p>
      SSR is fine, but the script throws synchronously once the island tries to hydrate. The defensive try/catch around <code>hydrate()</code> catches it and swaps the island for the
      failure stub — the rest of the page (already rendered on the server) is untouched.
    </p>
  </header>
  <ThrowOnClient mochi:hydrate label="ThrowOnClient" />
</section>

<section class="mode">
  <header>
    <h2>4. <code>mochi:defer</code> — server island throw</h2>
    <p>
      Server islands render at a separate endpoint. When that render throws, the endpoint catches it and returns a 200 with an error stub — so the browser doesn't burn its retry
      budget on a deterministic failure. The user-supplied loading children stay until the response arrives.
    </p>
  </header>
  <ThrowOnServerIsland mochi:defer label="ThrowOnServerIsland">
    <div class="placeholder">Loading from server…</div>
  </ThrowOnServerIsland>
</section>

<section class="mode">
  <header>
    <h2>
      5. <code>mochi:defer</code> — healthy island, inner <code>mochi:hydrate</code> client throw
    </h2>
    <p>
      The server island itself renders successfully — it's the inner <code>mochi:hydrate</code>
      child that throws once it tries to hydrate on the client. The child's auto-boundary catches its own failure, so the rest of the server island content stays intact.
    </p>
  </header>
  <HealthyServerIsland mochi:defer>
    <div class="placeholder">Loading from server…</div>
  </HealthyServerIsland>
</section>
Styles
<style>
  .mode {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin-bottom: 2.5rem;
  }

  .mode header {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
  }

  .mode h2 {
    margin: 0;
    font-size: 1.05rem;
  }

  .mode h2 code,
  .mode p code {
    font-family: var(--font-mono);
    background: var(--surface-muted);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 0.1em 0.35em;
    border-radius: 4px;
    font-size: 0.85em;
    font-weight: 500;
  }

  .mode p {
    margin: 0;
    color: var(--text-muted);
    font-size: 0.95rem;
    line-height: 1.5;
  }

  .placeholder {
    padding: 1rem;
    border: 2px dashed var(--border-strong);
    border-radius: var(--radius-md);
    color: var(--text-subtle);
    text-align: center;
    font-size: 0.9rem;
  }

  .caught {
    padding: 0.75rem 1rem;
    border: 2px dashed var(--badge-warning-text, #c80);
    background: var(--badge-warning-bg, #fff7e6);
    color: var(--badge-warning-text, #8a4d00);
    border-radius: var(--radius-md);
    font-size: 0.9rem;
  }

  .caught code {
    font-family: var(--font-mono);
    font-size: 0.85em;
  }
</style>