This page zooms into one control inside the broader Vue & Svelte Global Error Handlers guide: the svelte:boundary element that Svelte 5 shipped to close a long-standing gap in the framework’s crash-recovery story.
The exact failure this page solves #
Before Svelte 5, a component that threw during render had no local recovery path. Svelte’s runtime unwound the failing component tree and, depending on where the throw originated, could take the entire mounted app down with it — a single malformed API response feeding a chart component was enough to blank the whole page. Framework peers had addressed this years earlier: React shipped class-based error boundaries, and Vue exposed onErrorCaptured for the same purpose (see handling async errors in Vue 3 with onErrorCaptured for the Vue equivalent). Svelte 5 closes the gap with svelte:boundary, a template element that wraps a subtree, renders a failed snippet in its place when that subtree throws during render or inside an effect, and exposes an (error, reset) pair so you can log the failure and let the user retry without reloading the page.
This page covers exactly that mechanism: how to declare a boundary, how the failed snippet and reset() interact, and — just as importantly — which categories of error the boundary does not catch, so you don’t build a false sense of safety around it.
Zero-to-working implementation #
The component below wraps a widget that occasionally throws during render (for example, an assertion on malformed props). It logs the failure, shows a fallback with a retry button, and forces a clean remount on retry.
<!-- WidgetHost.svelte -->
<script>
import RiskyWidget from './RiskyWidget.svelte';
// Bumping `key` forces Svelte to destroy and recreate the failed
// subtree on retry, instead of re-rendering the same broken instance.
let key = $state(0);
let crashCount = $state(0);
function handleError(error, reset) {
crashCount += 1;
console.error('[WidgetHost] boundary caught:', error);
reportToTelemetry({ message: error.message, crashCount });
// Intentionally NOT calling reset() here — the failed snippet's
// button drives the retry, so the user controls recovery timing.
}
function reportToTelemetry(payload) {
fetch('/api/client-errors', {
method: 'POST',
body: JSON.stringify(payload),
}).catch(() => {}); // telemetry must never itself throw
}
</script>
<svelte:boundary onerror={handleError}>
{#key key}
<RiskyWidget />
{/key}
{#snippet failed(error, reset)}
<div class="widget-fallback" role="alert">
<p>This widget crashed: {error.message}</p>
<button
onclick={() => {
key += 1; // force a fresh RiskyWidget instance
reset(); // clear the boundary's failed state
}}
>
Retry
</button>
</div>
{/snippet}
</svelte:boundary>
Copy this as-is, swap in your own risky child, and the step-by-step explanation below maps each piece back to the code.
Step-by-step explanation #
-
Wrap the risky subtree, not the page.
<svelte:boundary>sits directly aroundRiskyWidget, not at the app root. Only the wrapped subtree is replaced by thefailedsnippet on error, so the rest of the page — navigation, layout, sibling widgets — keeps running. -
The
failedsnippet is the fallback contract. Declared as a child of the boundary, it receives(error, reset)as arguments. Anything you render inside it — the error message, a retry button, a support link — becomes the visible state the moment the wrapped content throws. -
onerroris for side effects, not rendering. ThehandleErrorfunction fires once per catch, before thefailedsnippet paints. Use it to log to the console and forward structured data to telemetry; do not use it to mutate UI state that the snippet itself should own. -
reset()clears the boundary’s failed flag but does not remount by itself. Callingreset()tells the boundary to try rendering the wrapped content again using its existing state. If the same broken props or corrupted local state are still in place, the identical error can fire again on the very next render pass. -
The
keybump forces a genuine retry. Incrementingkeyinside the{#key key}block destroys the oldRiskyWidgetinstance and creates a new one from scratch, soreset()is retrying against a clean slate rather than the exact state that just crashed. -
Non-render errors need a separate net. The boundary’s catch is scoped to the render pass and
$effectexecution. Errors thrown fromonclick,onsubmit, or inside asetTimeout/awaitcallback happen outside that pass and must be handled withtry/catchor a globalwindow.onerror/unhandledrejectionlistener, exactly as the Vue & Svelte Global Error Handlers guide covers for the rest of the framework surface.
Edge cases #
| Scenario | Behavior | Mitigation |
|---|---|---|
Error thrown inside an onclick handler |
Boundary does not catch it; error reaches the browser’s default window.onerror path |
Wrap handler bodies in try/catch, or route them through a shared global handler alongside the component isolation techniques used elsewhere in the app |
await rejects inside an async function called from a component |
Boundary does not catch it; surfaces as an unhandled rejection | Catch explicitly at the call site, similar to the pattern in catching unhandled promise rejections globally in React |
reset() called without a key bump |
Same broken instance re-renders; error can recur on the next tick | Pair reset() with a state or key change so the retry starts from a clean instance |
Nested svelte:boundary elements |
The nearest ancestor boundary catches first; outer boundaries never see the error | Scope boundaries at the granularity you want isolated — see choosing a boundary granularity strategy |
| Error during server-side rendering | Boundary catches it identically and emits the failed snippet’s markup directly in the SSR response |
Keep the failed snippet free of client-only APIs (window, document) so it renders safely on the server |
Verification steps #
-
Force a render-time throw. Add a temporary
if (!props.data) throw new Error('missing data')insideRiskyWidgetand omit the prop. Confirm thefailedsnippet appears in place of the widget while the rest of the page stays interactive. -
Exercise the retry path. Click the Retry button and confirm, via a
console.loginRiskyWidget’s script, that a brand-new component instance mounts rather than the same instance re-rendering — this confirms thekeybump is actually forcing a remount. -
Prove the coverage gap. Move the same
throwinto anonclickhandler instead of the render path. Confirm the boundary’sfailedsnippet does not appear and that the error instead surfaces in the console via the browser’s default unhandled-error reporting, matching the edge case above. -
Check the SSR path. Render the page server-side with the throwing condition active and view the raw HTML response (curl or “View Source”). Confirm the fallback markup from the
failedsnippet is present in the initial payload, not just injected after hydration. -
Confirm telemetry fires once per crash. Watch the Network tab for the
/api/client-errorsrequest while repeating the crash-and-retry cycle; the count of POST requests should match the number of times the boundary actually caught an error, not the number of retries clicked.
Frequently asked questions #
Does svelte:boundary catch errors thrown inside an onclick handler?
No. The boundary only catches errors thrown synchronously during a component’s render pass or inside its effects. An error thrown from a DOM event handler runs outside that render pass entirely, so it bypasses the boundary and reaches the browser’s default global error handling instead — the same gap that makes a paired window.onerror listener necessary.
What happens if the error recurs immediately after calling reset?
Calling reset() clears the boundary’s failed flag and re-renders the wrapped content using whatever state it already had. If that state — corrupted props, a stale store value — is unchanged, the identical error can throw again on the next render, making the retry look inert. Pairing reset() with a key increment on the wrapped subtree forces a fresh component instance so the retry isn’t repeating the exact conditions that just failed.
Does svelte:boundary work the same way during server-side rendering?
Yes, with one nuance: during SSR the boundary catches the same render-time errors and writes the failed snippet’s markup straight into the server-rendered response, so the user’s first paint already shows the fallback rather than a blank subtree. If the client subsequently hydrates and the same code path throws again, the boundary on the client catches it independently — the two catches are not linked.
Related #
- Vue & Svelte Global Error Handlers — parent guide covering both frameworks’ global and component-level error-handling primitives
- Handling Async Errors in Vue 3 with onErrorCaptured — the closest Vue equivalent, including its own async-coverage limits
- Framework-Specific Crash Recovery & Error Handlers — the section overview tying together React, Vue, Svelte, and Next.js/Nuxt error-handling primitives