Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8ca5d84
breaking: allow `handleError` to influence status code
dummdidumm Jun 24, 2026
ec110fb
Update documentation/docs/30-advanced/25-errors.md
dummdidumm Jun 24, 2026
b7f82a8
chore: autofix lint
github-actions[bot] Jun 24, 2026
3609842
lint
dummdidumm Jun 25, 2026
7a185d8
Update packages/kit/test/apps/basics/test/cross-platform/test.js
dummdidumm Jun 25, 2026
d06fb46
Merge branch 'version-3' into handle-error-status
teemingc Jun 25, 2026
3a4ada7
fix
teemingc Jun 25, 2026
46ba4b9
Merge branch 'version-3' into handle-error-status
teemingc Jun 25, 2026
e41b8c9
Merge branch 'version-3' into handle-error-status
teemingc Jun 26, 2026
adbdaf5
tweak error/App.Error shape
dummdidumm Jun 29, 2026
7003c3a
fix tests
dummdidumm Jun 29, 2026
e67120f
regenerate
dummdidumm Jun 29, 2026
60cd975
handleValidationError
dummdidumm Jun 29, 2026
04c3ca9
Fix: `HandleValidationError` return type dropped its `MaybePromise` w…
vercel[bot] Jun 29, 2026
c75db89
simplify
dummdidumm Jun 29, 2026
16f552d
fix
dummdidumm Jun 29, 2026
3708328
unnecessary
dummdidumm Jun 30, 2026
e37bf7b
Merge branch 'version-3' into handle-error-status
dummdidumm Jun 30, 2026
737dd82
simplify
dummdidumm Jun 30, 2026
231d11e
another
Rich-Harris Jul 1, 2026
c34b084
another
Rich-Harris Jul 1, 2026
9b8da37
bit more
Rich-Harris Jul 1, 2026
6134aa0
Fix: Hydrating an SSR'd error page yields `page.status === 200` inste…
vercel[bot] Jul 1, 2026
a530dd3
Update packages/kit/src/exports/index.js
Rich-Harris Jul 1, 2026
b79730a
gah apparently this falls through? could have sworn it worked a momen…
Rich-Harris Jul 1, 2026
4eb034a
make sure unexpected errors on form submissions on the client are rou…
dummdidumm Jul 1, 2026
ae9e34e
missed a spot
dummdidumm Jul 1, 2026
196812d
goddammit
Rich-Harris Jul 1, 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
6 changes: 6 additions & 0 deletions .changeset/ripe-tires-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@sveltejs/kit": major
---

breaking: allow `handleError` to influence status code

21 changes: 21 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,27 @@ 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`.

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

```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'
};
}

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

## 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.
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
],
"scripts": {
"lint": "prettier --config ../../.prettierrc --check .",
"check": "tsc && cd ./test/types && tsc",
"check": "tsc && cd ./test/types && tsc && cd ./app-error-enhanced && tsc",
"check:all": "tsc && pnpm -r --filter=\"./**\" check",
"format": "prettier --config ../../.prettierrc --write .",
"test": "pnpm test:unit && pnpm test:integration",
Expand Down
31 changes: 24 additions & 7 deletions packages/kit/src/exports/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ export { VERSION } from '../version.js';
* return an error response without invoking `handleError`.
* 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 {App.Error} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
* @param {Omit<App.Error, 'status'> & { status?: App.Error['status'] }} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
* @overload
* @param {number} status
* @param {App.Error} body
* @param {Omit<App.Error, 'status'> & { status?: App.Error['status'] }} body
* @return {never}
* @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
* @throws {Error} If the provided status is invalid (not between 400 and 599).
Comment thread
Rich-Harris marked this conversation as resolved.
Expand All @@ -40,10 +40,10 @@ export { VERSION } from '../version.js';
* return an error response without invoking `handleError`.
* 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 {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body] An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
* @param {{ status: number; message: string } extends App.Error ? string | void | undefined : never} body The error message.
* @overload
* @param {number} status
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body]
* @param {{ status: number; message: string } extends App.Error ? string | void | undefined : never} body
* @return {never}
* @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
* @throws {Error} If the provided status is invalid (not between 400 and 599).
Expand All @@ -54,17 +54,34 @@ export { VERSION } from '../version.js';
* return an error response without invoking `handleError`.
* 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 {{ message: string } extends App.Error ? App.Error | string | undefined : never} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
* @param {string} body The error message.
* @param {{ status: number; message: string } extends App.Error ? never : Omit<App.Error, 'status' | 'message'>} properties Additional properties of the App.Error type.
* @overload
* @param {number} status
* @param {string} body
* @param {{ status: number; message: string } extends App.Error ? never : Omit<App.Error, 'status' | 'message'>} properties
* @return {never}
* @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
* @throws {Error} If the provided status is invalid (not between 400 and 599).
*/
/**
* 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`.
* 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} [body] An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
* @param {any} [properties] Additional properties of the App.Error type when passing a string message.
* @return {never}
* @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
* @throws {Error} If the provided status is invalid (not between 400 and 599).
*/
export function error(status, body) {
export function error(status, body, properties) {
if ((!BROWSER || DEV) && (isNaN(status) || status < 400 || status > 599)) {
throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`);
}

throw new HttpError(status, body);
throw new HttpError(status, body, properties);
}

/**
Expand Down
40 changes: 39 additions & 1 deletion packages/kit/src/exports/index.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isRedirect, normalizeUrl, redirect } from './index.js';
import { error, isHttpError, isRedirect, normalizeUrl, redirect } from './index.js';
import { assert, describe, it } from 'vitest';

describe('normalizeUrl', () => {
Expand Down Expand Up @@ -74,3 +74,41 @@ describe('redirect', () => {
);
});
});

describe('error', () => {
it('adds status to the body', () => {
try {
error(418, 'teapot');
assert.fail('Expected error to throw');
} catch (e) {
if (!isHttpError(e)) {
assert.fail('Expected an HttpError');
}

assert.equal(e.status, 418);
assert.deepEqual(e.body, { message: 'teapot', status: 418 });
}
});

it('merges additional body properties', () => {
try {
// @ts-expect-error
error(400, 'Bad request', { code: 'bad_request' });
assert.fail('Expected error to throw');
} catch (e) {
if (!isHttpError(e)) {
assert.fail('Expected an HttpError');
}

assert.equal(e.status, 400);
assert.deepEqual(
e.body,
/** @type {any} */ ({
code: 'bad_request',
message: 'Bad request',
status: 400
})
);
}
});
});
11 changes: 6 additions & 5 deletions packages/kit/src/exports/internal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
export class HttpError {
/**
* @param {number} status
* @param {{message: string} extends App.Error ? (App.Error | string | undefined) : App.Error} body
* @param {Omit<App.Error, 'status'> | string | undefined} body
* @param {Omit<App.Error, 'status' | 'message'>} [properties]
*/
constructor(status, body) {
constructor(status, body, properties) {
this.status = status;
if (typeof body === 'string') {
this.body = { message: body };
this.body = { ...properties, message: body, status };
} else if (body) {
this.body = body;
this.body = { ...body, status };
Comment thread
Rich-Harris marked this conversation as resolved.
} else {
this.body = { message: `Error: ${status}` };
this.body = { message: `Error: ${status}`, status };
}
}

Expand Down
16 changes: 12 additions & 4 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export { PrerenderOption } from '../types/private.js';
// @ts-ignore this is an optional peer dependency so could be missing. Written like this so dts-buddy preserves the ts-ignore
type Span = import('@opentelemetry/api').Span;

type AppErrorWithOptionalStatus = Omit<App.Error, 'status'> & { status?: App.Error['status'] };

/**
* [Adapters](https://svelte.dev/docs/kit/adapters) are responsible for taking the production build and turning it into something that can be deployed to a platform of your choosing.
*/
Expand Down Expand Up @@ -951,34 +953,40 @@ export type Handle = (input: {
*
* If an unexpected error is thrown during loading or rendering, this function will be called with the error and the event.
* Make sure that this function _never_ throws an error.
*
* The returned object can include a `status` property to override the HTTP status code used in the response.
Comment thread
dummdidumm marked this conversation as resolved.
* If omitted, the status defaults to 500.
*/
export type HandleServerError = (input: {
error: unknown;
event: RequestEvent;
status: number;
message: string;
}) => MaybePromise<void | App.Error>;
}) => MaybePromise<void | AppErrorWithOptionalStatus>;

/**
* The [`handleValidationError`](https://svelte.dev/docs/kit/hooks#Server-hooks-handleValidationError) hook runs when the argument to a remote function fails validation.
*
* It will be called with the validation issues and the event, and must return an object shape that matches `App.Error`.
*/
export type HandleValidationError<Issue extends StandardSchemaV1.Issue = StandardSchemaV1.Issue> =
(input: { issues: Issue[]; event: RequestEvent }) => MaybePromise<App.Error>;
(input: { issues: Issue[]; event: RequestEvent }) => MaybePromise<AppErrorWithOptionalStatus>;

/**
* The client-side [`handleError`](https://svelte.dev/docs/kit/hooks#Shared-hooks-handleError) hook runs when an unexpected error is thrown while navigating.
*
* If an unexpected error is thrown during loading or the following render, this function will be called with the error and the event.
* Make sure that this function _never_ throws an error.
*
* The returned object can include a `status` property to override the HTTP status code used in the response.
Comment thread
Rich-Harris marked this conversation as resolved.
* If omitted, the status defaults to 500.
*/
export type HandleClientError = (input: {
error: unknown;
event: NavigationEvent;
status: number;
message: string;
}) => MaybePromise<void | App.Error>;
}) => MaybePromise<void | AppErrorWithOptionalStatus>;

/**
* The [`handleFetch`](https://svelte.dev/docs/kit/hooks#Server-hooks-handleFetch) hook allows you to modify (or replace) the result of an [`event.fetch`](https://svelte.dev/docs/kit/load#Making-fetch-requests) call that runs on the server (or during prerendering) inside an endpoint, `load`, `action`, `handle`, `handleError` or `reroute`.
Expand Down Expand Up @@ -1834,7 +1842,7 @@ export type ActionResult<
| { type: 'success'; status: number; data?: Success }
| { type: 'failure'; status: number; data?: Failure }
| { type: 'redirect'; status: number; location: string }
| { type: 'error'; status?: number; error: any };
| { type: 'error'; status?: number; error: App.Error };

/**
* The object returned by the [`error`](https://svelte.dev/docs/kit/@sveltejs-kit#error) function.
Expand Down
11 changes: 9 additions & 2 deletions packages/kit/src/runtime/app/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as devalue from 'devalue';
import { BROWSER, DEV } from 'esm-env';
import { noop } from '../../utils/functions.js';
import { invalidateAll } from './navigation.js';
import { app as client_app, applyAction } from '../client/client.js';
import { app as client_app, applyAction, handle_error } from '../client/client.js';
import { app as server_app } from '../server/app.js';

export { applyAction };
Expand Down Expand Up @@ -201,7 +201,14 @@ export function enhance(form_element, submit = noop) {
if (result.type === 'error') result.status = response.status;
} catch (error) {
if (/** @type {any} */ (error)?.name === 'AbortError') return;
result = { type: 'error', error };
result = {
type: 'error',
error: await handle_error(error, {
params: {},
route: { id: null },
url: new URL(location.href)
})
};
}

await callback({
Expand Down
9 changes: 3 additions & 6 deletions packages/kit/src/runtime/app/server/remote/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
import { noop } from '../../../../utils/functions.js';
import { SharedIterator } from '../../../../utils/shared-iterator.js';
import { handle_error_and_jsonify } from '../../../server/utils.js';
import { HttpError, SvelteKitError } from '@sveltejs/kit/internal';

/**
* Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call.
Expand Down Expand Up @@ -347,13 +346,11 @@ function batch(validate_or_fn, maybe_fn) {
const data = get_result(arg, i);
return { type: 'result', data };
} catch (error) {
const transformed = await handle_error_and_jsonify(event, state, options, error);

return {
type: 'error',
error: await handle_error_and_jsonify(event, state, options, error),
status:
error instanceof HttpError || error instanceof SvelteKitError
? error.status
: 500
error: transformed
};
}
})
Expand Down
12 changes: 5 additions & 7 deletions packages/kit/src/runtime/app/server/remote/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,11 @@ export function create_validator(validate_or_fn, maybe_fn) {

// if the `issues` field exists, the validation failed
if (result.issues) {
error(
400,
await state.handleValidationError({
issues: result.issues,
event
})
);
const body = await state.handleValidationError({
issues: result.issues,
event
});
error(body.status ?? 400, body);
}

return result.value;
Expand Down
Loading
Loading