How to: catch render errors with a boundary
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, 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>
Section titled “Svelte: <svelte:boundary>”Basic usage
Section titled “Basic usage”Wrap the part of the tree that might throw during render, and provide a
failed snippet as the fallback:
<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 thefailedsnippet takes its place. - Calling
reset()— handed to you by bothonerrorand thefailedsnippet — 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
Section titled “Capturing reset() for a retry button”The trap: reset arrives as a snippet parameter, and this looks reasonable
but silently doesn’t work:
{#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:
{#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:
failedBag: reset => { control.reset = reset; return { testID: 'failed' };},What’s proven, not just compiled
Section titled “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
failedsnippet renders,onerrorfires exactly once, and the thrown subtree is fully gone, not merely hidden behind the fallback; reset()called from inside thefailedsnippet 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>
Section titled “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
Section titled “Basic usage”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,
fallbackreceives the error and areset()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
Section titled “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:
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
Section titled “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.