Skip to main content

Hooks

Edit this page on GitHub

'Hooks' are app-wide functions you declare that SvelteKit will call in response to specific events, giving you fine-grained control over the framework's behaviour.

There are two hooks files, both optional:

  • src/hooks.server.js — your app's server hooks
  • src/hooks.client.js — your app's client hooks

Code in these modules will run when the application starts up, making them useful for initializing database clients and so on.

You can configure the location of these files with config.kit.files.hooks.

Server hooks

The following hooks can be added to src/hooks.server.js:

handle

This function runs every time the SvelteKit server receives a request — whether that happens while the app is running, or during prerendering — and determines the response. It receives an event object representing the request and a function called resolve, which renders the route and generates a Response. This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing routes programmatically, for example).

ts
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
if (event.url.pathname.startsWith('/custom')) {
return new Response('custom response');
}
 
const response = await resolve(event);
return response;
}
ts
import type { Handle } from '@sveltejs/kit';
 
export const handle = (async ({ event, resolve }) => {
if (event.url.pathname.startsWith('/custom')) {
return new Response('custom response');
}
 
const response = await resolve(event);
return response;
}) satisfies Handle;

Requests for static assets — which includes pages that were already prerendered — are not handled by SvelteKit.

If unimplemented, defaults to ({ event, resolve }) => resolve(event). To add custom data to the request, which is passed to handlers in +server.js and server-only load functions, populate the event.locals object, as shown below.

ts
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
event.locals.user = await getUserInformation(event.cookies.get('sessionid'));
 
const response = await resolve(event);
response.headers.set('x-custom-header', 'potato');
 
return response;
}
ts
import type { Handle } from '@sveltejs/kit';
 
export const handle = (async ({ event, resolve }) => {
event.locals.user = await getUserInformation(event.cookies.get('sessionid'));
 
const response = await resolve(event);
response.headers.set('x-custom-header', 'potato');
 
return response;
}) satisfies Handle;

You can add call multiple handle functions with the sequence helper function.

resolve also supports a second, optional parameter that gives you more control over how the response will be rendered. That parameter is an object that can have the following fields:

  • transformPageChunk(opts: { html: string, done: boolean }): MaybePromise<string | undefined> — applies custom transforms to HTML. If done is true, it's the final chunk. Chunks are not guaranteed to be well-formed HTML (they could include an element's opening tag but not its closing tag, for example) but they will always be split at sensible boundaries such as %sveltekit.head% or layout/page components.
  • filterSerializedResponseHeaders(name: string, value: string): boolean — determines which headers should be included in serialized responses when a load function loads a resource with fetch. By default, none will be included.
  • preload(input: { type: 'js' | 'css' | 'font' | 'asset', path: string }): boolean — determines what files should be added to the <head> tag to preload it. The method is called with each file that was found at build time while constructing the code chunks — so if you for example have import './styles.css in your +page.svelte, preload will be called with the resolved path to that CSS file when visiting that page. Note that in dev mode preload is not called, since it depends on analysis that happens at build time. Preloading can improve performance by downloading assets sooner, but it can also hurt if too much is downloaded unnecessarily. By default, js and css files will be preloaded. asset files are not preloaded at all currently, but we may add this later after evaluating feedback.
ts
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('old', 'new'),
filterSerializedResponseHeaders: (name) => name.startsWith('x-'),
preload: ({ type, path }) => type === 'js' || path.includes('/important/')
});
 
return response;
}
ts
import type { Handle } from '@sveltejs/kit';
 
export const handle = (async ({ event, resolve }) => {
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('old', 'new'),
filterSerializedResponseHeaders: (name) => name.startsWith('x-'),
preload: ({ type, path }) => type === 'js' || path.includes('/important/')
});
 
return response;
}) satisfies Handle;

Note that resolve(...) will never throw an error, it will always return a Promise<Response> with the appropriate status code. If an error is thrown elsewhere during handle, it is treated as fatal, and SvelteKit will respond with a JSON representation of the error or a fallback error page — which can be customised via src/error.html — depending on the Accept header. You can read more about error handling here.

handleFetch

This function allows you to modify (or replace) a fetch request that happens inside a load function that runs on the server (or during pre-rendering).

Or your load function might make a request to a public URL like https://api.yourapp.com when the user performs a client-side navigation to the respective page, but during SSR it might make sense to hit the API directly (bypassing whatever proxies and load balancers sit between it and the public internet).

ts
/** @type {import('@sveltejs/kit').HandleFetch} */
export async function handleFetch({ request, fetch }) {
if (request.url.startsWith('https://api.yourapp.com/')) {
// clone the original request, but change the URL
request = new Request(
request.url.replace('https://api.yourapp.com/', 'http://localhost:9999/'),
request
);
}
 
return fetch(request);
}
ts
import type { HandleFetch } from '@sveltejs/kit';
 
export const handleFetch = (async ({ request, fetch }) => {
if (request.url.startsWith('https://api.yourapp.com/')) {
// clone the original request, but change the URL
request = new Request(
request.url.replace('https://api.yourapp.com/', 'http://localhost:9999/'),
request
);
}
 
return fetch(request);
}) satisfies HandleFetch;

Credentials

For same-origin requests, SvelteKit's fetch implementation will forward cookie and authorization headers unless the credentials option is set to "omit".

For cross-origin requests, cookie will be included if the request URL belongs to a subdomain of the app — for example if your app is on my-domain.com, and your API is on api.my-domain.com, cookies will be included in the request.

If your app and your API are on sibling subdomains — www.my-domain.com and api.my-domain.com for example — then a cookie belonging to a common parent domain like my-domain.com will not be included, because SvelteKit has no way to know which domain the cookie belongs to. In these cases you will need to manually include the cookie using handleFetch:

ts
/** @type {import('@sveltejs/kit').HandleFetch} */
export async function handleFetch({ event, request, fetch }) {
if (request.url.startsWith('https://api.my-domain.com/')) {
request.headers.set('cookie', event.request.headers.get('cookie'));
Argument of type 'string | null' is not assignable to parameter of type 'string'. Type 'null' is not assignable to type 'string'.2345Argument of type 'string | null' is not assignable to parameter of type 'string'. Type 'null' is not assignable to type 'string'.
}
 
return fetch(request);
}
ts
import type { HandleFetch } from '@sveltejs/kit';
 
export const handleFetch = (async ({ event, request, fetch }) => {
if (request.url.startsWith('https://api.my-domain.com/')) {
request.headers.set('cookie', event.request.headers.get('cookie'));
Argument of type 'string | null' is not assignable to parameter of type 'string'. Type 'null' is not assignable to type 'string'.2345Argument of type 'string | null' is not assignable to parameter of type 'string'. Type 'null' is not assignable to type 'string'.
}
 
return fetch(request);
}) satisfies HandleFetch;

Shared hooks

The following can be added to src/hooks.server.js and src/hooks.client.js:

handleError

If an unexpected error is thrown during loading or rendering, this function will be called with the error and the event. 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 becomes the value of $page.error. It defaults to { message: 'Not Found' } in case of a 404 (you can detect them through event.route.id being null) and to { message: 'Internal Error' } for everything else. To make this type-safe, you can customize the expected shape by declaring an App.Error interface (which must include message: string, to guarantee sensible fallback behavior).

The following code shows an example of typing the error shape as { message: string; code: string } and returning it accordingly from the handleError functions:

ts
declare namespace App {
interface Error {
message: string;
code: string;
}
}
ts
/** @type {import('@sveltejs/kit').HandleServerError} */
export function handleError({ error, event }) {
// example integration with https://sentry.io/
Sentry.captureException(error, { event });
Type '{ message: string; code: any; }' is not assignable to type 'MaybePromise<void | Error>'. Object literal may only specify known properties, and 'code' does not exist in type 'Error | Promise<void | Error>'.
Property 'code' does not exist on type '{}'.
2322
2339
Type '{ message: string; code: any; }' is not assignable to type 'MaybePromise<void | Error>'. Object literal may only specify known properties, and 'code' does not exist in type 'Error | Promise<void | Error>'.
Property 'code' does not exist on type '{}'.
 
return {
message: 'Whoops!',
code: error?.code ?? 'UNKNOWN'
};
}
ts
import type { HandleServerError } from '@sveltejs/kit';
 
export const handleError = (({ error, event }) => {
// example integration with https://sentry.io/
Sentry.captureException(error, { event });
Property 'code' does not exist on type '{}'.2339Property 'code' does not exist on type '{}'.
 
return {
message: 'Whoops!',
code: error?.code ?? 'UNKNOWN'
};
}) satisfies HandleServerError;
ts
/** @type {import('@sveltejs/kit').HandleClientError} */
export function handleError({ error, event }) {
// example integration with https://sentry.io/
Sentry.captureException(error, { event });
Type '{ message: string; code: any; }' is not assignable to type 'MaybePromise<void | Error>'. Object literal may only specify known properties, and 'code' does not exist in type 'Error | Promise<void | Error>'.
Property 'code' does not exist on type '{}'.
2322
2339
Type '{ message: string; code: any; }' is not assignable to type 'MaybePromise<void | Error>'. Object literal may only specify known properties, and 'code' does not exist in type 'Error | Promise<void | Error>'.
Property 'code' does not exist on type '{}'.
 
return {
message: 'Whoops!',
code: error?.code ?? 'UNKNOWN'
};
}
ts
import type { HandleClientError } from '@sveltejs/kit';
 
export const handleError = (({ error, event }) => {
// example integration with https://sentry.io/
Sentry.captureException(error, { event });
Property 'code' does not exist on type '{}'.2339Property 'code' does not exist on type '{}'.
 
return {
message: 'Whoops!',
code: error?.code ?? 'UNKNOWN'
};
}) satisfies HandleClientError;

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 function imported from @sveltejs/kit).

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.

Make sure that handleError never throws an error

next Errors