Running code for every request in SvelteKit
Hooks in SvelteKit give you a place to run code before every request hits your routes. Hooks in SvelteKit: a practical Svelte & SvelteKit guide walks through the server-side hooks that let you authenticate users, attach data to requests, catch errors globally, and compose multiple middleware functions. If you've worked with Express middleware or similar request pipelines in other frameworks, SvelteKit hooks follow a familiar pattern—but with some important differences we'll cover here.
handle — Your first line of defense for every request
The handle hook runs on the server for every incoming request. It receives an event object containing request details, and it must return a response. This is where most teams attach authentication, logging, or request metadata.
Here's a realistic setup where we fetch the current user from a session cookie and attach it to the request:
// src/hooks.server.ts
import { getUser } from '$lib/auth'
export const handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get('session')
if (sessionId) {
event.locals.user = await getUser(sessionId)
} else {
event.locals.user = null
}
return resolve(event)
}
The resolve(event) call tells SvelteKit to continue processing—routing to your page, calling load functions, and so on. Anything you attach to event.locals will be available downstream in your load functions and server actions.
A common production scenario: you want to add request tracing. Attach a request ID to every request, then include it in logs:
import { v4 as uuid } from 'uuid'
export const handle = async ({ event, resolve }) => {
event.locals.requestId = uuid()
const startTime = Date.now()
const response = await resolve(event)
const duration = Date.now() - startTime
console.log(`[${event.locals.requestId}] ${event.request.method} ${event.url.pathname} - ${duration}ms`)
return response
}
This simple approach gives you observability across your entire application without touching individual routes.
locals — Sharing request data with loads and actions
The event.locals object is your bridge between hooks and your page/component code. Anything you store there is available in page load functions and server actions within that request.
Let's extend our authentication example. After fetching the user in handle, access it in a load function:
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) {
throw error(401, 'Unauthorized')
}
return {
user: locals.user,
dashboard: await getDashboardData(locals.user.id)
}
}
Or in a server action:
// src/routes/profile/+page.server.ts
import { fail } from '@sveltejs/kit'
export const actions = {
updateProfile: async ({ request, locals }) => {
if (!locals.user) {
return fail(401, { message: 'Not authenticated' })
}
const formData = await request.formData()
await updateUserProfile(locals.user.id, Object.fromEntries(formData))
return { success: true }
}
}
locals is request-scoped, so each request gets its own isolated copy. This prevents data leaking between concurrent requests—a critical property for server-side rendering.
handleError — Global error logging and transformation
The handleError hook fires whenever an error is thrown in a load function, server action, or route handler. Use it to log unexpected errors, transform them for the client, or send them to an error tracking service.
Basic setup:
// src/hooks.server.ts
import * as Sentry from '@sentry/sveltekit'
export const handleError = async ({ error, event }) => {
const errorId = crypto.randomUUID()
console.error(`[${errorId}] Error in ${event.request.method} ${event.url.pathname}`, error)
Sentry.captureException(error, {
tags: { requestId: event.locals.requestId }
})
// Return a safe message to the client
return {
message: 'An unexpected error occurred',
errorId
}
}
This logs the full error server-side but returns a generic message to the browser. The errorId lets users report "Error ABC123" so you can search logs and find the root cause.
In a real system, you might differentiate between expected errors and bugs:
import { HttpError } from '$lib/errors'
export const handleError = async ({ error, event }) => {
if (error instanceof HttpError) {
// Expected error: send it as-is
return {
message: error.message,
status: error.status
}
}
// Unexpected bug: log and anonymize
const errorId = crypto.randomUUID()
console.error(`[${errorId}]`, error)
return {
message: 'An unexpected error occurred',
errorId
}
}
sequence — Composing multiple hooks
Real applications often need multiple middleware functions. SvelteKit's sequence helper composes them into a single handle.
Imagine you have authentication, request logging, and CORS headers as separate concerns:
// src/hooks.server.ts
import { sequence } from '@sveltejs/kit/hooks'
import { authenticateUser } from '$lib/auth'
import { logRequest } from '$lib/logging'
const auth = async ({ event, resolve }) => {
event.locals.user = await authenticateUser(event.cookies.get('session'))
return resolve(event)
}
const logging = async ({ event, resolve }) => {
const start = Date.now()
const response = await resolve(event)
const duration = Date.now() - start
logRequest({
method: event.request.method,
path: event.url.pathname,
duration,
userId: event.locals.user?.id
})
return response
}
const cors = async ({ event, resolve }) => {
const response = await resolve(event)
if (event.request.headers.get('origin') === 'https://trusted-domain.com') {
response.headers.set('Access-Control-Allow-Origin', '*')
}
return response
}
export const handle = sequence(auth, logging, cors)
Each hook runs in order. The resolve(event) call passes control to the next hook in the chain, finally reaching your route handler. The response flows back through the chain in reverse—so cors can modify headers added by other hooks.
Order matters. Put authentication early so downstream hooks can trust event.locals.user. Put logging near the end so it captures the final response.
Common mistakes
- Blocking requests unnecessarily: If you're doing database queries in
handlefor every request, consider caching or deferring to load functions instead.handleruns for static assets too, so keep it fast. - Mutating locals without thinking about types: TypeScript won't catch typos in
event.locals.userId. Declare your locals shape inapp.d.tsto get type safety. - Forgetting to return
resolve(event): If you forget to call or return it, your routes never execute. Always return a response fromhandle. - Mixing client and server hooks:
handleis server-only. For client-side setup, usehooks.client.tsinstead—different file, different purpose. - Not setting locals for unauthenticated users: If you only set
event.locals.userwhen authenticated, downstream code must check for undefined. Always initialize locals with a default.
Typing locals in app.d.ts
To get TypeScript autocomplete on event.locals, declare its shape:
// src/app.d.ts
declare global {
namespace App {
interface Locals {
user: { id: string; email: string } | null
requestId: string
}
}
}
export {}
Now TypeScript will warn you if you try to access event.locals.userId (typo) instead of event.locals.user.id.
Key takeaways
- Use
handlefor authentication, logging, and any logic that needs to run on every request. - Attach data to
event.localsto share it with load functions and actions. - Use
handleErrorfor global error handling—log server-side, return safe messages to the client. - Compose multiple hooks with
sequenceto keep concerns separate and testable. - Declare your locals shape in
app.d.tsfor full TypeScript support. - Keep
handlefast—it runs on every request, including static assets. - Remember that
localsis request-scoped; each request gets its own isolated copy.






Comments
Loading comments…