diff --git a/.changeset/handle-error-all-errors.md b/.changeset/handle-error-all-errors.md new file mode 100644 index 000000000000..f40f914478e3 --- /dev/null +++ b/.changeset/handle-error-all-errors.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: run all errors through the `handleError` hook diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index 490a09446d23..52666999ba4b 100644 --- a/documentation/docs/20-core-concepts/20-load.md +++ b/documentation/docs/20-core-concepts/20-load.md @@ -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 @@ -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 diff --git a/documentation/docs/20-core-concepts/60-remote-functions.md b/documentation/docs/20-core-concepts/60-remote-functions.md index fefdefa7e0fe..61499df2a485 100644 --- a/documentation/docs/20-core-concepts/60-remote-functions.md +++ b/documentation/docs/20-core-concepts/60-remote-functions.md @@ -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 @@ -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 diff --git a/documentation/docs/30-advanced/10-advanced-routing.md b/documentation/docs/30-advanced/10-advanced-routing.md index 9647754182d8..e947b35e2017 100644 --- a/documentation/docs/30-advanced/10-advanced-routing.md +++ b/documentation/docs/30-advanced/10-advanced-routing.md @@ -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 diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index 6d1275d8665d..44672c6392dc 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -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; } } @@ -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 @@ -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 { @@ -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. diff --git a/documentation/docs/30-advanced/25-errors.md b/documentation/docs/30-advanced/25-errors.md index fcb0f2daabeb..2538dfbea3fd 100644 --- a/documentation/docs/30-advanced/25-errors.md +++ b/documentation/docs/30-advanced/25-errors.md @@ -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 @@ -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