🍡 mochi

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

Error boundaries

Every mochi:hydrate and mochi:hydrate:visible island is auto-wrapped in <svelte:boundary>. A throw in one island no longer takes down the whole page render — the failed island is replaced by a small marker (or hidden in production) and the rest of the page continues. No opt-in, no configuration.

What’s wrapped

  • mochi:hydrate — wrapped.
  • mochi:hydrate:visible — wrapped.
  • mochi:defer — handled by the server-island endpoint instead (see below).
  • Top-level page render throws — go to the configured errorPage, not to a boundary. Mochi does not insert a page-level boundary; if you want a section to degrade gracefully, author <svelte:boundary> yourself.

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 (synchronous script throws, $effect, $derived) — caught via transformError on hydrate(). Note: client-side errors are logged to the browser console but do not emit island:error (the event bus is server-side only).
  • Synchronous failures in the island’s bundle import or in hydrate() itself — caught by a defensive try/catch and rendered as the same failure stub.

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 (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 — it stops the client’s retry loop from hammering a deterministic failure. Whatever fallback children you passed stay visible until the response arrives.

Author your own boundary

Mochi makes <svelte:boundary> work during SSR — stock Svelte boundaries are no-ops on the server without transformError, which Mochi passes for you. Use it anywhere — wrap a hand-written non-island component, or wrap a chunk of a page when you want bespoke degradation:

{#snippet failed(error: Error)}
  <p>Something went wrong: {error.message}</p>
{/snippet}

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

Observability — island:error event

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

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 (matches the island-id attribute); set for 'server' failures, undefined for 'hydratable' SSR failures
kind'hydratable' (SSR throw inside a hydratable island) or 'server' (server-island endpoint render). The MochiIslandErrorKind type also reserves 'client-hydrate', but client-side errors are not currently emitted to the event bus.
messageError message — safe to forward
stackStack trace; populated only when development: true

See also