Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d425712
breaking: pass all errors through `handleError`, update the function
elliott-with-the-longest-name-on-github Aug 5, 2026
e8c5c10
Update 10-advanced-routing.md
elliott-with-the-longest-name-on-github Aug 6, 2026
966fa10
Update 60-remote-functions.md
elliott-with-the-longest-name-on-github Aug 6, 2026
759d302
Update 20-hooks.md
elliott-with-the-longest-name-on-github Aug 6, 2026
86223c8
Update 20-hooks.md
elliott-with-the-longest-name-on-github Aug 6, 2026
b283f04
Update 20-hooks.md
elliott-with-the-longest-name-on-github Aug 6, 2026
3d6672b
Update 25-errors.md
elliott-with-the-longest-name-on-github Aug 6, 2026
ab1c7cc
Update 25-errors.md
elliott-with-the-longest-name-on-github Aug 6, 2026
63889ca
Update errors.js
elliott-with-the-longest-name-on-github Aug 6, 2026
4ad379f
Update client.js
elliott-with-the-longest-name-on-github Aug 6, 2026
31ee6f9
Update hooks.server.js
elliott-with-the-longest-name-on-github Aug 6, 2026
860a0ce
misc feedback
elliott-with-the-longest-name-on-github Aug 6, 2026
7e3f7f7
docs
elliott-with-the-longest-name-on-github Aug 6, 2026
75302df
misc
elliott-with-the-longest-name-on-github Aug 6, 2026
d8088be
references
elliott-with-the-longest-name-on-github Aug 6, 2026
2de710a
bah
elliott-with-the-longest-name-on-github Aug 6, 2026
796f10d
lint
elliott-with-the-longest-name-on-github Aug 6, 2026
e29e997
types
elliott-with-the-longest-name-on-github Aug 6, 2026
21ba8e9
ugh
elliott-with-the-longest-name-on-github Aug 6, 2026
495e1e7
test: add handleError kind routes
elliott-with-the-longest-name-on-github Aug 7, 2026
51bcaa7
update names
elliott-with-the-longest-name-on-github Aug 10, 2026
0841f37
docs: rename error categories
elliott-with-the-longest-name-on-github Aug 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/handle-error-all-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': major
---

breaking: run all errors through the `handleError` hook
4 changes: 2 additions & 2 deletions documentation/docs/20-core-concepts/20-load.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ export async function load({ params, parent }) {

## Errors

If an error is thrown during `load`, the nearest [`+error.svelte`](routing#error) will be rendered. For [_expected_](errors#Expected-errors) errors, use the `error` helper from `@sveltejs/kit` to specify the HTTP status code and an optional message:
If an error is thrown during `load`, the nearest [`+error.svelte`](routing#error) will be rendered. For [app errors](errors#App-errors), use the `error` helper from `@sveltejs/kit` to specify the HTTP status code and an optional message:

```js
/// file: src/routes/admin/+layout.server.js
Expand Down Expand Up @@ -453,7 +453,7 @@ export function load({ locals }) {

Calling `error(...)` will throw an exception, making it easy to stop execution from inside helper functions.

If an [_unexpected_](errors#Unexpected-errors) error is thrown, SvelteKit will invoke [`handleError`](hooks#handleError) and treat it as a 500 Internal Error.
Every error is passed to the [`handleError`](hooks#handleError) hook. An [unknown error](errors#Unknown-errors) is treated as a 500 Internal Error unless the hook says otherwise.

> [!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

Expand Down
4 changes: 3 additions & 1 deletion documentation/docs/20-core-concepts/60-remote-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,7 @@ As long as _you're_ not passing invalid data to your remote functions, there are
- the function signature changed between deployments, and some users are currently on an older version of your app
- someone is trying to attack your site by poking your exposed endpoints with bad data

In the second case, we don't want to give the attacker any help, so SvelteKit will generate a generic [400 Bad Request](https://http.dog/400) response. You can control the message by implementing the [`handleValidationError`](hooks#handleValidationError) server hook, which, like [`handleError`](hooks#handleError), must return an [`App.Error`](errors#Type-safety) (which defaults to `{ message: string }`):
In the second case, we don't want to give the attacker any help, so SvelteKit will generate a generic [400 Bad Request](https://http.dog/400) response. You can control the message by implementing the [`handleValidationError`](hooks#handleValidationError) server hook, which must return an object matching [`App.Error`](errors#Type-safety) (which defaults to `{ status: number, message: string }`, with `status` defaulting to `400` if you omit it):

```js
/// file: src/hooks.server.js
Expand All @@ -1248,6 +1248,8 @@ export function handleValidationError({ event, issues }) {
}
```

The object you return becomes the body of an [app error](errors#App-errors), which then passes through [`handleError`](hooks#handleError) with `kind: 'app'`.

If you know what you're doing and want to opt out of validation, you can pass the string `'unchecked'` in place of a schema:

```ts
Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/30-advanced/10-advanced-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function load(event) {
}
```

> [!NOTE] If you don't handle 404 cases, they will appear in [`handleError`](hooks#handleError)
> [!NOTE] If you don't handle 404 cases, they will appear in [`handleError`](hooks#handleError) as [framework errors](errors#Framework-errors), with `kind: 'framework'`. Otherwise, they will appear with `kind: 'app'`.

## Optional parameters

Expand Down
59 changes: 49 additions & 10 deletions documentation/docs/30-advanced/20-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,25 +177,42 @@ export function handleValidationError({ issues }) {

Be thoughtful about what information you expose here, as the most likely reason for validation to fail is that someone is sending malicious requests to your server.

The object you return here becomes the body of an [app error](errors#App-errors), which means it subsequently passes through [`handleError`](hooks#handleError) as an error with `kind: 'app'`.

## handleError

> [!NOTE] Can be added to `src/hooks.server.js` and `src/hooks.client.js`

If an [unexpected error](errors#Unexpected-errors) is thrown during loading, rendering, or from an endpoint, this function will be called with the `error`, `event`, `status` code and `message`. This allows for two things:
This function is called for _every_ error thrown while loading, rendering, or responding to a request. This allows for two things:

- you can log the error
- you can generate a custom representation of the error that is safe to show to users, omitting sensitive details like messages and stack traces. The returned value, which defaults to `{ message }`, becomes the value of `page.error`.
- you can generate a custom representation of the error that is safe to show to users, omitting sensitive details like messages and stack traces. The returned value becomes the value of `page.error`.

Alongside the `event`, the hook receives a `kind` discriminant that tells you where the error came from, and the `error` itself:

- `'app'` — the error came from your app via [`error(...)`](@sveltejs-kit#error)
- `error` is the error body, which matches [`App.Error`](types#Error)
- defaults to the error body itself
- `'framework'` — the error came from SvelteKit, such as a 404, 405 or 413
- `error` is `{ status, message }`, where `message` is safe text like `Not Found`
- defaults to that same `{ status, message }`
- `'unknown'` — we don't know what went wrong; the error was thrown by your code, or code it calls
- `error` is the thrown value, which may contain information unsafe to expose
- defaults to `{ status: 500, message: 'Internal Error' }`

The next section, [Errors](errors), explains these categories in more detail. Errors from [`handleValidationError`](#handleValidationError) arrive as _expected_ errors. Redirects are not errors, and never reach the hook.

For errors thrown from your code (or library code called by your code) the status will be 500 and the message will be "Internal Error". While `error.message` may contain sensitive information that should not be exposed to users, `message` is safe (albeit meaningless to the average user).
The hook returns an object matching [`App.Error`](types#Error), in which `status` and `message` are optional — return them only to override the defaults in the list above.

To add more information to the `page.error` object in a type-safe way, you can customize the expected shape by declaring an `App.Error` interface (which must include `message: string`, to guarantee sensible fallback behavior). This allows you to — for example — append a tracking ID for users to quote in correspondence with your technical support staff:
> [!NOTE] If you augment `App.Error` with additional _required_ properties, the hook must return them.

To add more information to the `page.error` object in a type-safe way, augment the existing `App.Error` interface with your additional properties. The built-in `status` and `message` properties are already present and do not need to be redeclared. For example, you can add a tracking ID for users to quote when contacting support:

```ts
/// file: src/app.d.ts
declare global {
namespace App {
interface Error {
message: string;
errorId: string;
}
}
Expand All @@ -220,14 +237,28 @@ import * as Sentry from '@sentry/sveltekit';
Sentry.init({/*...*/})

/** @type {import('@sveltejs/kit').HandleServerError} */
export async function handleError({ error, event, status, message }) {
export async function handleError({ kind, error, event }) {
if (kind === 'app') {
// you created this error with `error(...)`, so it already
// matches `App.Error` — pass it through unchanged
return error;
}

const errorId = crypto.randomUUID();

if (kind === 'framework') {
// a 404 (or similar) — `error.status` and `error.message` are safe to
// expose, so we keep them and just add our own property
return { ...error, errorId };
}

// example integration with https://sentry.io/
Sentry.captureException(error, {
extra: { event, errorId, status }
extra: { event, errorId }
});

// `status` and `message` are optional — we only override `message`,
// so the status stays at its default of 500
return {
message: 'Whoops!',
errorId
Expand All @@ -251,12 +282,20 @@ import * as Sentry from '@sentry/sveltekit';
Sentry.init({/*...*/})

/** @type {import('@sveltejs/kit').HandleClientError} */
export async function handleError({ error, event, status, message }) {
export async function handleError({ kind, error, event }) {
if (kind === 'app') {
return error;
}

const errorId = crypto.randomUUID();

if (kind === 'framework') {
return { ...error, errorId };
}

// example integration with https://sentry.io/
Sentry.captureException(error, {
extra: { event, errorId, status }
extra: { event, errorId }
});

return {
Expand All @@ -268,7 +307,7 @@ export async function handleError({ error, event, status, message }) {

> [!NOTE] In `src/hooks.client.js`, the type of `handleError` is `HandleClientError` instead of `HandleServerError`, and `event` is a `NavigationEvent` rather than a `RequestEvent`.

This function is not called for _expected_ errors (those thrown with the [`error`](@sveltejs-kit#error) function imported from `@sveltejs/kit`).
Errors that were already transformed by the server-side hook are not passed to the client-side hook a second time.

During development, if an error occurs because of a syntax error in your Svelte code, the passed in error has a `frame` property appended highlighting the location of the error.

Expand Down
47 changes: 31 additions & 16 deletions documentation/docs/30-advanced/25-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ 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 `{ status: number, message: string }` objects by default.
Every error passes through the [`handleError`](hooks#handleError) hook — which can log it and customise it — before it is rendered. The hook's `kind` property identifies where the error came from: your app (`'app'`), SvelteKit (`'framework'`) or an unknown source (`'unknown'`). By default, all are represented as simple `{ status: number, message: string }` objects.

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)).

## Expected errors
## App errors

An _expected_ error is one created with the [`error`](@sveltejs-kit#error) helper imported from `@sveltejs/kit`:
An _app_ error is one created with the [`error`](@sveltejs-kit#error) helper imported from `@sveltejs/kit`:

```js
/// file: src/routes/blog/[slug]/+page.server.js
Expand Down Expand Up @@ -40,6 +40,8 @@ export async function load({ params }) {

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`.

On its way there, the error passes through the [`handleError`](hooks#handleError) hook with `kind: 'app'`. Since the shape of the error is determined by your app, it is considered safe to expose, and the hook can pass it through unchanged.

```svelte
<!--- file: src/routes/+error.svelte --->
<script>
Expand Down Expand Up @@ -73,39 +75,52 @@ error(404, 'Not found', {

> [!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
## Unknown errors

An _unexpected_ error is any other exception that occurs while handling a request. Since these can contain sensitive information, unexpected error messages and stack traces are not exposed to users.
An _unknown_ error is any other exception that occurs while handling a request. Since these can contain sensitive information, unknown error messages and stack traces are not exposed to users.

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:
By default, unknown 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
{ "status": 500, "message": "Internal Error" }
```

Unexpected errors will go through the [`handleError`](hooks#handleError) hook, where you can add your own error handlingfor example, sending errors to a reporting service, or returning a custom error object which becomes the `error` prop passed to `+error.svelte`.
Unknown errors go through the [`handleError`](hooks#handleError) hook with `kind: 'unknown'`, because SvelteKit does not know what went wrong. There 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`. The value you receive is the raw thrown value, and nothing about it is exposed unless you choose to expose it.

You can override the HTTP status code used in the response by returning a `status` property:
Anything you return overrides the defaults, so you can — for example — use the type of the thrown error to determine the HTTP status code used in the response:

```js
/// file: src/hooks.server.js
// Assuming you have this ...
class NotFound extends Error {}

/** @type {import('@sveltejs/kit').HandleServerError} */
export function handleError({ error, event, status, message }) {
// ... you can do this
if (error instanceof NotFound) {
return {
status: 404,
message: 'Not found'
};
export function handleError({ kind, error, event }) {
if (kind === 'unknown') {
// ... you can do this
if (error instanceof NotFound) {
return {
status: 404,
message: 'Not found'
};
}

return { message: 'Something went wrong' };
}

return { message: 'Something went wrong' };
// app and framework errors are already safe to expose
return error;
}
```

## Framework errors

Some errors are generated by SvelteKit itself rather than by your code — a request for a page that doesn't exist (404), a `POST` request to a page without actions (405), a request body that exceeds the size limit (413), and so on.

These also go through `handleError`, with a `kind` of `'framework'`. The `error` you receive is a `{ status, message }` object whose `message` is a terse but safe description of what went wrong, such as `'Not Found'`, so it can be exposed to users as-is.

If you log errors inside `handleError`, remember that framework errors such as 404s are routine — you will usually want to gate logging on `kind === 'unknown'`.

## Error boundaries

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):
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/sync/write_client_manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
export const hooks = {
handleError: ${
client_hooks_file ? 'client_hooks.handleError || ' : ''
}(({ error }) => { console.error(error) }),
}(({ kind, error }) => { if (kind === 'unknown') { console.error(error); } }),
${client_hooks_file ? 'init: client_hooks.init,' : ''}
reroute: ${universal_hooks_file ? 'universal_hooks.reroute || ' : ''}(() => {}),
transport: ${universal_hooks_file ? 'universal_hooks.transport || ' : ''}{}
Expand Down
8 changes: 4 additions & 4 deletions packages/kit/src/exports/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export { defineParams } from './params.js';
/**
* Throws an error with a HTTP status code and an optional message.
* When called during request handling, this will cause SvelteKit to
* return an error response without invoking `handleError`.
* return an error response; the error will be passed to `handleError` as an _expected_ error.
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
* @param {string} [message] The error message.
Expand All @@ -38,7 +38,7 @@ export { defineParams } from './params.js';
/**
* Throws an error with a HTTP status code and an optional message.
* When called during request handling, this will cause SvelteKit to
* return an error response without invoking `handleError`.
* return an error response; the error will be passed to `handleError` as an _expected_ error.
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
* @param {string} message The error message.
Expand All @@ -54,7 +54,7 @@ export { defineParams } from './params.js';
/**
* Throws an error with a HTTP status code and an optional message.
* When called during request handling, this will cause SvelteKit to
* return an error response without invoking `handleError`.
* return an error response; the error will be passed to `handleError` as an _expected_ error.
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
* @deprecated Passing an `App.Error` body as the second argument is deprecated — pass the `message` as the second argument, and any additional properties as the third
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
Expand All @@ -69,7 +69,7 @@ export { defineParams } from './params.js';
/**
* Throws an error with a HTTP status code and an optional message.
* When called during request handling, this will cause SvelteKit to
* return an error response without invoking `handleError`.
* return an error response; the error will be passed to `handleError` as an _expected_ error.
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
* @param {any} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
* @param {any} [message] A string, or (deprecated) a partial App.Error object
Expand Down
9 changes: 9 additions & 0 deletions packages/kit/src/exports/internal/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ export class HttpError {
}
}

/**
* An `HttpError` whose body is already in its final, user-facing form — either produced by the
* `handleError` hook on the server and reconstructed here from the response, or authored directly
* by the client runtime. Unlike a plain `HttpError` (which represents a fresh `error(...)` call
* that the hook has yet to see), `handleError` must not run on it.
* @extends HttpError
*/
export class HandledHttpError extends HttpError {}

export class Redirect {
/**
* @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308} status
Expand Down
Loading
Loading