Skip to content
5 changes: 5 additions & 0 deletions .changeset/better-clocks-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': major
---

breaking: remove `experimental.handleRenderingErrors` flag
74 changes: 13 additions & 61 deletions documentation/docs/30-advanced/25-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Errors are an inevitable fact of software development. SvelteKit handles errors

## Error objects

SvelteKit distinguishes between expected and unexpected errors, both of which are represented as simple `{ message: string }` objects by default.
SvelteKit distinguishes between expected and unexpected errors, both of which are represented as simple `{ status: number, message: string }` objects by default.

You can add additional properties, like a `code` or a tracking `id`, as shown in the examples below. (When using TypeScript this requires you to redefine the `Error` type as described in [type safety](errors#Type-safety)).

Expand All @@ -31,27 +31,25 @@ export async function load({ params }) {
const post = await db.getPost(params.slug);

if (!post) {
error(404, {
message: 'Not found'
});
error(404, 'Not found');
}

return { post };
}
```

This throws an exception that SvelteKit catches, causing it to set the response status code to 404 and render an [`+error.svelte`](routing#error) component, where `page.error` is the object provided as the second argument to `error(...)`.
This throws an exception that SvelteKit catches, causing it to set the response status code to 404 and render an [`+error.svelte`](routing#error) component, where the `error` is an `App.Error` object with the provided `status` and `message`.

```svelte
<!--- file: src/routes/+error.svelte --->
<script>
import { page } from '$app/state';
let { error } = $props();
Comment thread
Rich-Harris marked this conversation as resolved.
</script>

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

You can add extra properties to the error object if needed...
You can add extra properties to the error object if needed:

```js
// @filename: ambient.d.ts
Expand All @@ -68,21 +66,11 @@ export {}
// @filename: index.js
import { error } from '@sveltejs/kit';
// ---cut---
error(404, {
message: 'Not found',
error(404, 'Not found', {
+++code: 'NOT_FOUND'+++
});
```

...otherwise, for convenience, you can pass a string as the second argument:

```js
import { error } from '@sveltejs/kit';
// ---cut---
---error(404, { message: 'Not found' });---
+++error(404, 'Not found');+++
```

Comment thread
Rich-Harris marked this conversation as resolved.
> [!NOTE] [In SvelteKit 1.x](migrating-to-sveltekit-2#redirect-and-error-are-no-longer-thrown-by-you) you had to `throw` the `error` yourself

## Unexpected errors
Expand All @@ -92,10 +80,10 @@ An _unexpected_ error is any other exception that occurs while handling a reques
By default, unexpected errors are printed to the console (or, in production, your server logs), while the error that is exposed to the user has a generic shape:

```json
{ "message": "Internal Error" }
{ "status": 500, "message": "Internal Error" }
```

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`.
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 the `error` prop passed to `+error.svelte`.

You can override the HTTP status code used in the response by returning a `status` property:

Expand All @@ -118,51 +106,15 @@ export function handleError({ error, event, status, message }) {
}
```

## 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: vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [
sveltekit({
experimental: {
handleRenderingErrors: true
}
})
]
});
```

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>
```
## Error boundaries

The same applies for other error boundaries you define in your code:
Errors that occur during `load` or rendering (for example inside a component's `<script>` block or template) bubble up to the nearest `+error.svelte` component. To handle errors at a more granular level, you can use a [`<svelte:boundary>`](../svelte/svelte-boundary):

```svelte
<svelte:boundary>
...
{#snippet failed(error: App.Error)}
<!-- error went through handleError and is of type App.Error -->
<!-- error went through the `handleError` hook and is of type `App.Error` -->
{error.message}
{/snippet}
</svelte:boundary>
Expand Down Expand Up @@ -213,7 +165,7 @@ declare global {
export {};
```

This interface always includes a `message: string` property.
This interface always includes `status: number` and `message: string` properties.

## Further reading

Expand Down
3 changes: 1 addition & 2 deletions packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ const get_defaults = (prefix = '') => ({
},
experimental: {
remoteFunctions: false,
forkPreloads: false,
handleRenderingErrors: false
forkPreloads: false
},
files: {
src: join(prefix, 'src'),
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const validate_kit_options = object({
),
remoteFunctions: boolean(false),
forkPreloads: boolean(false),
handleRenderingErrors: boolean(false)
handleRenderingErrors: removed()
}),

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 @@ -37,7 +37,7 @@ export function create(config, root) {

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

Expand Down
85 changes: 28 additions & 57 deletions packages/kit/src/core/sync/write_root.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ import { dedent, 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, config, output) {
export function write_root(manifest_data, output) {
// TODO remove default layout altogether

const use_boundaries = config.kit.experimental.handleRenderingErrors;

const max_depth = Math.max(
...manifest_data.routes.map((route) =>
route.page ? route.page.layouts.filter(Boolean).length + 1 : 0
Expand All @@ -22,58 +19,32 @@ export function write_root(manifest_data, config, output) {
levels.push(i);
}

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

if (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>
// 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
const pyramid = dedent`
{#snippet pyramid(depth)}
{@const Pyramid = constructors[depth]}
Comment thread
Rich-Harris marked this conversation as resolved.
{#snippet failed(error)}
{@const ErrorPage = errors[depth]}
Comment thread
Rich-Harris marked this conversation as resolved.
<ErrorPage {error} />
{/snippet}

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

while (l--) {
pyramid = dedent`
{#if constructors[${l + 1}]}
{@const Pyramid_${l} = constructors[${l}]}
<!-- svelte-ignore binding_property_non_reactive -->
<Pyramid_${l} bind:this={components[${l}]} data={data_${l}} {form} params={page.params}>
${pyramid}
</Pyramid_${l}>
{:else}
{@const Pyramid_${l} = constructors[${l}]}
<!-- svelte-ignore binding_property_non_reactive -->
<Pyramid_${l} bind:this={components[${l}]} data={data_${l}} {form} params={page.params} />
{/if}
`;
}
}
<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)}
`;

write_if_changed(
`${output}/root.svelte`,
Expand All @@ -83,10 +54,10 @@ export function write_root(manifest_data, config, output) {
<script>
import { afterNavigate } from '$app/navigation';

let { page, constructors, components = [], form, ${use_boundaries ? 'errors = [], error, ' : ''}${levels
let { page, constructors, components = [], form, errors = [], error, ${levels
.map((l) => `data_${l} = null`)
.join(', ')} } = $props();
${use_boundaries ? `let data = $derived({${levels.map((l) => `'${l}': data_${l}`).join(', ')}})` : ''}
let data = $derived({${levels.map((l) => `'${l}': data_${l}`).join(', ')}});

let mounted = $state(false);
let navigated = $state(false);
Expand Down
1 change: 0 additions & 1 deletion packages/kit/src/core/sync/write_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ 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: 0 additions & 9 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,15 +509,6 @@ 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
4 changes: 3 additions & 1 deletion packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,9 @@ function kit({ svelte_config }) {
__SVELTEKIT_CLIENT_ROUTING__: s(kit.router.resolution === 'client'),
__SVELTEKIT_HASH_ROUTING__: s(kit.router.type === 'hash'),
__SVELTEKIT_SERVER_TRACING_ENABLED__: s(kit.tracing.server),
__SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__: s(kit.experimental.handleRenderingErrors),
__SVELTEKIT_SUPPORTS_ASYNC__: s(
svelte_config.compilerOptions?.experimental?.async ?? false
),
__SVELTEKIT_ROOT__: s(root),
__SVELTEKIT_DEV__: s(!is_build)
};
Expand Down
21 changes: 9 additions & 12 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -709,16 +709,13 @@ async function initialize(result, target, hydrate) {
hydrate,
// Svelte 5 specific: asynchronously instantiate the component, i.e. don't call flushSync
sync: false,
// Svelte 5 specific: transformError allows to transform errors before they are passed to boundaries
transformError: __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__
? /** @param {unknown} e */ async (e) => {
const error = await handle_error(e, current.nav);
rendering_error = { error, status: error.status };
page.error = error;
page.status = rendering_error.status;
return error;
}
: undefined
transformError: /** @param {unknown} e */ async (e) => {
const error = await handle_error(e, current.nav);
rendering_error = { error, status: error.status };
page.error = error;
page.status = rendering_error.status;
return error;
}
});

// Wait for a microtask in case svelte experimental async is enabled,
Expand Down Expand Up @@ -802,7 +799,7 @@ async function get_navigation_result_from_branch({
}
};

if (errors && __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__) {
if (errors) {
let last_idx = -1;
result.props.errors = await Promise.all(
// eslint-disable-next-line @typescript-eslint/await-thenable
Expand All @@ -824,7 +821,7 @@ async function get_navigation_result_from_branch({
);
}

if (error && __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__) {
if (error) {
result.props.error = error;
}

Expand Down
Loading
Loading