SvelteKit had a dirty secret for years. If a component threw during server-side rendering, your users got a blank 500 page. No graceful degradation, no partial recovery — just a white screen and a stack trace in your server logs. Kit 2.54.0, which shipped last week, changes this in a way that's both overdue and surprisingly well-designed.
The 500-or-Nothing Problem
You have a dashboard with six widgets. One of them hits a flaky third-party API. On the client, <svelte:boundary> catches the error, shows a "retry" button, and the other five widgets keep working. On the server? The entire page blows up. Every widget, every piece of static content, every nav element — gone.
This wasn't a bug. Server rendering is a single pass. Once HTML starts streaming, there's no clean way to retroactively wrap a chunk in an error state. React solved this with Suspense boundaries during SSR back in React 18. Vue has onErrorCaptured. Svelte's answer was just... silence.
One Config Flag
The fix is behind an experimental gate:
// svelte.config.js
const config = {
kit: {
experimental: {
handleRenderingErrors: true
}
}
};
export default config;
That's the whole migration. When this flag is on, the framework wraps your route components in error boundaries during SSR. A component that throws gets caught by the nearest +error.svelte page instead of killing the request.
How the Error Actually Reaches Your Component
This is where it gets interesting, because rendering errors behave differently from load errors in ways that affect how you write your error pages.
When a load function fails, the framework hasn't started rendering yet. It can cleanly redirect to your error page with full context — $page.error is populated, the status code is set, everything works as you'd expect. A rendering error happens mid-stream. The page object is partially constructed. If you have parallel rendering, multiple boundaries might catch distinct errors at the same time.
The team made a pragmatic decision: rendering errors bypass the page store entirely. The error arrives as a prop.
<!-- +error.svelte -->
<script>
// load errors → $page.error (still works)
// rendering errors → direct prop
let { error } = $props();
</script>
<div class="error-state">
<h2>Something broke</h2>
<p>{error.message}</p>
</div>
The error still flows through your handleError hook first, so Sentry or Datadog or whatever you use gets notified before the user sees anything. The transformed error object — not the raw exception — is what reaches the component. This matters because server errors often contain database connection strings, internal file paths, or other information you absolutely don't want serialized into HTML.
For apps that need even more control over sanitization, Svelte 5.51 introduced transformError as a first-class option on the render function:
const { head, body } = await render(App, {
transformError: (error) => {
console.error('SSR failure:', error);
return {
message: error instanceof DatabaseError
? 'Service temporarily unavailable'
: error.message
};
}
});
The return value gets JSON-stringified and embedded in the HTML for hydration. Keep it small. Keep it boring.
Granular Recovery with <svelte:boundary>
Route-level error pages are the safety net. For finer control, <svelte:boundary> now actually works during server rendering:
<svelte:boundary>
<WeatherWidget />
{#snippet failed(error, reset)}
<div class="widget-error">
Weather unavailable — <button
</div>
{/snippet}
</svelte:boundary>
<svelte:boundary>
<StockTicker />
{#snippet failed(error)}
<p>Market data could not be loaded.</p>
{/snippet}
</svelte:boundary>
On the client, this was already the deal — the reset function re-renders the boundary's children. On the server, the failed snippet renders in place of the broken component, and the rest of the page continues building normally. Your five working widgets ship to the browser intact.
One thing to know: the reset button in the server-rendered HTML is inert until hydration finishes. The framework doesn't attempt to re-run the server render from the client — it shows the fallback and lets hydration handle recovery. No magic, just reasonable defaults.
The Limits
Don't wrap everything in boundaries and assume you're crash-proof. These are explicitly out of scope:
Event handlers — a click callback that throws hits no boundary. Different execution context entirely.
Async work after render —
setTimeoutcallbacks, late-resolvingfetchcalls inside$effect. These are client-side by nature.loaderrors — still routed through the existing error system. Boundaries cover component evaluation only.
The mental model: if the code runs as part of building or updating DOM, boundaries catch it. If it runs in response to user interaction or delayed async resolution, it doesn't.
React's Been Here Before
React shipped class-based error boundaries in version 16 — nearly a decade ago. But they've never had a hook-based equivalent, so you either write a class component in 2026 or install react-error-boundary. Server-side, renderToPipeableStream has an onError callback, but it's a stream-level escape hatch, not a component-level recovery mechanism.
SvelteKit's version is arguably tighter: one config flag, identical <svelte:boundary> syntax on client and server, transformError as a named concept rather than a stream callback. Fewer moving parts.
Ship It or Wait?
The experimental label is honest — they might rename a prop or adjust how concurrent errors merge. But the core behavior is well-scoped: catch render errors, show fallback UI, sanitize what goes to the client. There's not much surface area for a breaking change.
If your app has third-party integrations, user-generated content that feeds into rendering, or anything else that could throw during SSR — and that's basically every production app — upgrade and flip the flag.
npm update @sveltejs/kit@latest
The worst case is adjusting an import path in a future point release. The best case is your users stop seeing 500 pages when a weather widget has a bad day.