🍡 mochi

SSR framework for Svelte 5 + Bun with islands-based selective hydration

On this page

Error boundaries

Mochi auto-wraps every mochi:hydrate and mochi:hydrate:visible island in <svelte:boundary>. A throw inside one island no longer takes down the page render — the failed island is replaced by a <mochi-island-failure> stub and the rest of the page continues. No opt-in, no configuration.

Do NOT assume Mochi wraps your page in a boundary; instead, author <svelte:boundary> yourself where you want graceful degradation.

What’s wrapped

  • mochi:hydrate — wrapped.
  • mochi:hydrate:visible — wrapped.
  • mochi:defer — handled by the server-island endpoint instead, not by a boundary.
  • Top-level page render throws — go to the configured errorPage. See Error handling.

What gets caught

  • Synchronous SSR throws inside the island.
  • Async SSR throws (await Promise.reject(...) in a top-level <script>).
  • Client-side throws after hydration ($effect, $derived, synchronous script throws) — caught via transformError on hydrate().
  • Synchronous failures in the island’s bundle import or in hydrate() itself — caught by a defensive try/catch and rendered as the same stub.

Client-side throws log to the browser console but do NOT emit island:error; the event bus is server-side only.

Visual behaviour

In development the failed island is replaced by a dashed-red <mochi-island-failure> marker showing the component name and error message. In production the element is hidden via display: none, so end users see a clean gap instead of a stack trace.

Server islands (mochi:defer)

The /island/:name endpoint try/catches the SSR render and returns 200 plus a <mochi-island-failure> stub. The 200 is intentional — a 5xx would trigger the client’s retry loop against a deterministic failure. Whatever fallback children you passed stay visible until the response arrives.

Author your own boundary

Mochi passes transformError to render() and hydrate(), which is what makes <svelte:boundary> functional during SSR (stock Svelte boundaries are no-ops on the server without it). Use <svelte:boundary> anywhere — wrap a non-island component, or wrap a chunk of a page when you want bespoke degradation:

<!-- file: src/SomePage.svelte -->
{#snippet failed(error: Error)}
  <p>Something went wrong: {error.message}</p>
{/snippet}

<svelte:boundary {failed}>
  <SomePieceThatMightThrow />
</svelte:boundary>

Do NOT rely on a bare <svelte:boundary> outside Mochi to catch SSR throws; instead, mount it inside a Mochi-rendered tree so transformError flows through.

island:error event

Every server-side island failure emits island:error on mochiEvents. Subscribe to forward failures to error tracking:

// file: src/index.ts
import { mochiEvents } from 'mochi-framework';

mochiEvents.on('island:error', ({ componentName, kind, message, stack }) => {
  tracker.capture(message, { componentName, kind, stack });
});
FieldDescription
componentNameIsland component name.
islandIdPer-island id, carried inside the signed props envelope; set for 'server' failures, undefined for 'hydratable'.
kind'hydratable' (SSR throw inside a hydratable island) or 'server' (server-island endpoint render).
messageError message — safe to forward.
stackStack trace; populated only when development: true.

The MochiIslandErrorKind type also reserves 'client-hydrate', but client-side errors are not currently emitted to the event bus.

See also