Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f9077f6
feat: allow error boundaries to catch errors on the server
dummdidumm Feb 12, 2026
4dc9665
better code output; pass error prop
dummdidumm Feb 13, 2026
59867d4
ensure NavigationEvent is set
dummdidumm Feb 13, 2026
ed183f7
test it
dummdidumm Feb 13, 2026
4747589
fixes/tweaks
dummdidumm Feb 17, 2026
081cc30
root.svelte snippet simplifcation
dummdidumm Feb 17, 2026
54c76be
fix data prop passing
dummdidumm Feb 17, 2026
4201313
fix test app, client/write_root/render fixes
dummdidumm Feb 17, 2026
893d207
root layout wraps root error component, not the other way around
dummdidumm Feb 17, 2026
61cfd7a
Merge branch 'main' into error-boundaries-server
dummdidumm Feb 21, 2026
2ad2d98
bump packages
dummdidumm Feb 21, 2026
46901d8
lint
dummdidumm Feb 21, 2026
8fcff53
docs
dummdidumm Feb 21, 2026
54d51d5
rename
dummdidumm Feb 21, 2026
51d4ac0
stfu eslint
dummdidumm Feb 21, 2026
2f1e4d3
missed a spot
dummdidumm Feb 22, 2026
21ee2cb
Apply suggestion from @dummdidumm
dummdidumm Feb 22, 2026
2c15482
update page.error/status
dummdidumm Feb 24, 2026
fe4aab5
Merge branch 'main' into error-boundaries-server
dummdidumm Feb 25, 2026
d738ed7
Fix: Documentation has typo in closing tag `</svelte::boundary>` with…
vercel[bot] Feb 26, 2026
6de75b6
Fix: In the `transformError` callback, `get_status(error)` is called …
vercel[bot] Feb 26, 2026
4546cb3
Fix: HTTP status codes (e.g., 403, 404) are lost and always replaced …
vercel[bot] Feb 26, 2026
0b46132
Merge branch 'main' into error-boundaries-server
Rich-Harris Mar 10, 2026
bb79369
Update documentation/docs/30-advanced/25-errors.md
dummdidumm Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cyan-walls-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: allow error boundaries to catch errors on the server
48 changes: 48 additions & 0 deletions documentation/docs/30-advanced/25-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,54 @@ By default, unexpected errors are printed to the console (or, in production, you

Unexpected errors will go through the [`handleError`](hooks#Shared-hooks-handleError) hook, where you can add your own error handling — for example, sending errors to a reporting service, or returning a custom error object which becomes `page.error`.

## Rendering errors

Ordinarily, if an error happens during server-side rendering (for example inside a component's `<script>` block or template), SvelteKit will return a 500 error page.

Since SvelteKit 2.54 and Svelte 5.53, you can change this by enabling the experimental `handleRenderingErrors` option in your config:

```js
/// file: svelte.config.js
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
experimental: {
handleRenderingErrors: true
}
}
};

export default config;
```

When this is enabled, SvelteKit will wrap your route components in an error boundary. If an error occurs during rendering, the nearest [`+error.svelte`](routing#error) page will be shown, just as if the error had occurred in a `load` function.

The error is first passed to [`handleError`](hooks#Shared-hooks-handleError), allowing you to report it and transform it, before the resulting object is passed to the `+error.svelte` component.

> [!NOTE]
> Since rendering errors occur after the page has started rendering, and multiple boundaries could in parallel catch distinct errors, the [`page`]($app-state#page) object (and its `error` property) will not be updated. Instead, the error is passed directly to the `+error.svelte` component as a prop.

```svelte
<!--- file: +error.svelte --->
<script>
let { error } = $props();
</script>

<h1>{error.message}</h1>
```

The same applies for other error boundaries you define in your code:

```svelte
<svelte:boundary>
...
{#snippet failed(error: App.Error)}
<!-- error went through handleError and is of type App.Error -->
{error.message}
{/snippet}
</svelte:boundary>
```

## Responses

If an error occurs inside `handle` or inside a [`+server.js`](routing#server) request handler, SvelteKit will respond with either a fallback error page or a JSON representation of the error object, depending on the request's `Accept` headers.
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ const get_defaults = (prefix = '') => ({
tracing: { server: false },
instrumentation: { server: false },
remoteFunctions: false,
forkPreloads: false
forkPreloads: false,
handleRenderingErrors: false
},
files: {
src: join(prefix, 'src'),
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ const options = object(
server: boolean(false)
}),
remoteFunctions: boolean(false),
forkPreloads: boolean(false)
forkPreloads: boolean(false),
handleRenderingErrors: boolean(false)
}),

files: object({
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/sync/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function create(config) {

write_client_manifest(config.kit, manifest_data, `${output}/client`);
write_server(config, output);
write_root(manifest_data, output);
write_root(manifest_data, config, output);
write_all_types(config, manifest_data);
write_non_ambient(config.kit, manifest_data);

Expand Down
47 changes: 41 additions & 6 deletions packages/kit/src/core/sync/write_root.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { dedent, isSvelte5Plus, write_if_changed } from './utils.js';

/**
* @param {import('types').ManifestData} manifest_data
* @param {import('types').ValidatedConfig} config
* @param {string} output
*/
export function write_root(manifest_data, output) {
export function write_root(manifest_data, config, output) {
// TODO remove default layout altogether

const use_boundaries = config.kit.experimental.handleRenderingErrors && isSvelte5Plus();

const max_depth = Math.max(
...manifest_data.routes.map((route) =>
route.page ? route.page.layouts.filter(Boolean).length + 1 : 0
Expand All @@ -20,17 +23,47 @@ export function write_root(manifest_data, output) {
}

let l = max_depth;
/** @type {string} */
let pyramid;

let pyramid = dedent`
if (isSvelte5Plus() && use_boundaries) {
// with the @const we force the data[depth] access to be derived, which is important to not fire updates needlessly
// TODO in Svelte 5 we should rethink the client.js side, we can likely make data a $state and only update indexes that changed there, simplifying this a lot
pyramid = dedent`
{#snippet pyramid(depth)}
{@const Pyramid = constructors[depth]}
{#snippet failed(error)}
{@const ErrorPage = errors[depth]}
<ErrorPage {error} />
{/snippet}
<svelte:boundary failed={errors[depth] ? failed : undefined}>
{#if constructors[depth + 1]}
{@const d = data[depth]}
<!-- svelte-ignore binding_property_non_reactive -->
<Pyramid bind:this={components[depth]} data={d} {form} params={page.params}>
{@render pyramid(depth + 1)}
</Pyramid>
{:else}
{@const d = data[depth]}
<!-- svelte-ignore binding_property_non_reactive -->
<Pyramid bind:this={components[depth]} data={d} {form} params={page.params} {error} />
{/if}
</svelte:boundary>
{/snippet}

{@render pyramid(0)}
`;
} else {
pyramid = dedent`
${
isSvelte5Plus()
? `<!-- svelte-ignore binding_property_non_reactive -->
<Pyramid_${l} bind:this={components[${l}]} data={data_${l}} {form} params={page.params} />`
: `<svelte:component this={constructors[${l}]} bind:this={components[${l}]} data={data_${l}} {form} params={page.params} />`
}`;

while (l--) {
pyramid = dedent`
while (l--) {
pyramid = dedent`
{#if constructors[${l + 1}]}
${
isSvelte5Plus()
Expand All @@ -57,6 +90,7 @@ export function write_root(manifest_data, output) {

{/if}
`;
}
}

write_if_changed(
Expand All @@ -72,9 +106,10 @@ export function write_root(manifest_data, output) {
${
isSvelte5Plus()
? dedent`
let { stores, page, constructors, components = [], form, ${levels
let { stores, page, constructors, components = [], form, ${use_boundaries ? 'errors = [], error, ' : ''}${levels
.map((l) => `data_${l} = null`)
.join(', ')} } = $props();
${use_boundaries ? `let data = $derived({${levels.map((l) => `'${l}': data_${l}`).join(', ')}})` : ''}
`
: dedent`
export let stores;
Expand Down Expand Up @@ -108,7 +143,7 @@ export function write_root(manifest_data, output) {
isSvelte5Plus()
? dedent`
$effect(() => {
stores;page;constructors;components;form;${levels.map((l) => `data_${l}`).join(';')};
stores;page;constructors;components;form;${use_boundaries ? 'errors;error;' : ''}${levels.map((l) => `data_${l}`).join(';')};
stores.page.notify();
});
`
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/core/sync/write_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const options = {
root,
service_worker: ${has_service_worker},
service_worker_options: ${config.kit.serviceWorker.register ? s(config.kit.serviceWorker.options) : 'null'},
server_error_boundaries: ${s(!!config.kit.experimental.handleRenderingErrors)},
templates: {
app: ({ head, body, assets, nonce, env }) => ${s(template)
.replace('%sveltekit.head%', '" + head + "')
Expand Down
9 changes: 9 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,15 @@ export interface KitConfig {
* @default false
*/
forkPreloads?: boolean;

/**
* Whether to enable the experimental handling of rendering errors.
* When enabled, `<svelte:boundary>` is used to wrap components at each level
* where there's an `+error.svelte`, rendering the error page if the component fails.
* In addition, error boundaries also work on the server and the error object goes through `handleError`.
* @default false
*/
handleRenderingErrors?: boolean;
};
/**
* Where to find various files within your project.
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,8 @@ async function kit({ svelte_config }) {
__SVELTEKIT_PATHS_RELATIVE__: s(kit.paths.relative),
__SVELTEKIT_CLIENT_ROUTING__: s(kit.router.resolution === 'client'),
__SVELTEKIT_HASH_ROUTING__: s(kit.router.type === 'hash'),
__SVELTEKIT_SERVER_TRACING_ENABLED__: s(kit.experimental.tracing.server)
__SVELTEKIT_SERVER_TRACING_ENABLED__: s(kit.experimental.tracing.server),
__SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__: s(kit.experimental.handleRenderingErrors)
};

if (is_build) {
Expand Down
Loading
Loading