# How to: catch render errors with a boundary

> Real error boundaries on Svelte and Solid - a fallback and reset(), sourced from each adapter's own tests.

Svelte 5 and Solid both ship a real error-boundary primitive -
`<svelte:boundary>` and solid-js's `<ErrorBoundary>`, re-exported verbatim
from the Solid adapter barrel. React and Vue don't: React's class-component
`componentDidCatch` boundary is not exposed as a documented pattern in the
[React API](/docs/api/react/), and Vue's nearest tool, `onErrorCaptured`, is a
different shape — a hook you write yourself, not a template construct with
its own fallback UI. If you need the same fallback-UI behavior on those
adapters today, you're on your own.

## Svelte: `<svelte:boundary>`

### Basic usage

Wrap the part of the tree that might throw during render, and provide a
`failed` snippet as the fallback:

```svelte
<svelte:boundary onerror={(error, reset) => console.error(error)}>
  <Child />

  {#snippet failed(error, reset)}
    <view testID="failed">
      <text>{error.message}</text>
    </view>
  {/snippet}
</svelte:boundary>
```

- Nothing throws → the boundary paints no native node of its own; children
  commit exactly as if it weren't there.
- A child throws during render → its subtree is torn down completely (not
  just hidden), `onerror(error, reset)` fires once, and the `failed` snippet
  takes its place.
- Calling `reset()` — handed to you by both `onerror` and the `failed` snippet
  — tears the failed subtree back down and re-mounts the children.

Source: `adapters/svelte/src/boundary.smoke.test.ts`, run against the real
Svelte compiler and asserted against the exact committed Fabric tree, not a
mock — these examples are transcribed from its real markup.

### Capturing `reset()` for a retry button

The trap: `reset` arrives as a snippet parameter, and this looks reasonable
but silently doesn't work:

```svelte
{#snippet failed(error, reset)}
  {@const captured = capture(reset)}
  ...
{/snippet}
```

`{@const}` compiles to `$.derived(...)`, and a derived value nothing reads is
never evaluated — the capture silently never runs. Route it through a prop
bag expression instead, since `set_custom_element_data` always evaluates its
argument:

```svelte
{#snippet failed(error, reset)}
  <symbiote-view p={control.failedBag(reset)}>
    <symbiote-text p={{}}>{error.message}</symbiote-text>
  </symbiote-view>
{/snippet}
```

where `failedBag` is a plain function stashing `reset` somewhere a retry
button can reach it:

```ts
failedBag: reset => {
  control.reset = reset;
  return { testID: 'failed' };
},
```

<Aside type="note">
  Snippet parameters also arrive as thunks in the compiled output — a `reset`
  captured this way is called as `reset()`, not read as `reset`.
</Aside>

### What's proven, not just compiled

`<svelte:boundary>` was compile-verified only before this — the preprocessor
allowed it and `tsc --build` saw nothing, but nothing in the repo had ever run
`$.boundary` for real. The smoke test covers, against exact committed child
lists rather than `toBeDefined()`:

- transparent render when nothing throws — the boundary contributes no native
  node of its own;
- a child component throwing during its own init: the `failed` snippet
  renders, `onerror` fires exactly once, and the thrown subtree is fully
  gone, not merely hidden behind the fallback;
- `reset()` called from inside the `failed` snippet restores the children,
  without leaving the failed subtree behind as a duplicate;
- a boundary wrapping a real `{#each}` list survives a reorder + grow +
  shrink in one update without losing track of its own anchor.

## Solid: `<ErrorBoundary>`

No adapter code at all - `ErrorBoundary` is solid-js's own primitive,
re-exported unchanged from `@symbiote-native/solid`. It catches only what
throws while its subtree is rendering or updating; an error thrown from an
event handler (an `onPress`) never reaches it, exactly like Svelte's
`onerror`.

### Basic usage

```tsx
import { ErrorBoundary, Show } from 'solid-js';

<ErrorBoundary
  fallback={(error, reset) => (
    <view>
      <text testID="failed">{`caught: ${error.message}`}</text>
      <ActionButton
        title="reset"
        onPress={() => {
          setExploding(false);
          reset();
        }}
      />
    </view>
  )}
>
  <Show when={exploding()} fallback={<text>subtree is healthy</text>}>
    <Exploder />
  </Show>
</ErrorBoundary>;
```

- Nothing throws -> the boundary paints no native node of its own.
- The child throws during render -> the subtree is torn down, `fallback`
  receives the error and a `reset()` callback, and paints in its place.
- `reset()` re-runs the subtree - it doesn't un-throw on its own, so clear
  whatever condition caused the throw (`setExploding(false)` above) before
  calling it, the same ordering Svelte's retry button needs.

### Event-handler errors need `catchError`, not the boundary

A throw inside `onPress` never reaches `<ErrorBoundary>` - solid-js's
`catchError` is the tool for that case instead:

```tsx
import { catchError } from 'solid-js';

const onPress = () =>
  catchError(
    () => {
      throw new Error('thrown inside a handler');
    },
    error => setCaught(error.message),
  );
```

Source: `examples/solid/components/api-playground/ErrorBoundaryDemo.tsx`
runs both side by side, since knowing which one applies is the whole
difficulty.

### What's tested

`adapters/solid/src/render-error-reporting.test.tsx` runs a real
`<ErrorBoundary>` against the engine's fake Fabric and asserts, not just
compiles:

- a boundary-caught error stays off the native redbox reporter - writing a
  boundary is the app saying "I'm handling this," so a full-screen redbox
  over the fallback it just painted would contradict that, matching React's
  and Vue's adapters;
- the fallback commits as the real child in place of the thrown subtree.

Verified via vitest against the fake Fabric tree, not yet exercised on a
real device.
