
Sveltekit Data Flow
- 21 installs
- 217 repo stars
- Updated August 3, 2026
- spences10/svelte-claude-skills
Helps with ai & agent building tasks.
About
sveltekit-data-flow is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- sveltekit-data-flow
- AI & Agent Building
- AI-coding skill
Sveltekit Data Flow by the numbers
- 21 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #10,307 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spences10/svelte-claude-skills --skill sveltekit-data-flowAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 21 |
|---|---|
| repo stars | ★ 217 |
| Last updated | August 3, 2026 |
| Repository | spences10/svelte-claude-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
SvelteKit Data Flow
Quick Start
Which file? Server-only (DB/secrets): +page.server.ts | Universal (runs both): +page.ts | API: +server.ts
Load decision: Need server resources? → server load | Need client APIs? → universal load
Form actions: Always +page.server.ts. Return fail() for errors, throw redirect() to navigate, throw error() for failures.
Example
// +page.server.ts
import { fail, redirect } from '@sveltejs/kit';
export const load = async ({ locals }) => {
const user = await db.users.get(locals.userId);
return { user }; // Must be JSON-serializable
};
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email');
if (!email) return fail(400, { email, missing: true });
await updateEmail(email);
throw redirect(303, '/success');
},
};Reference Files
- load-functions.md - Server vs
universal
- form-actions.md - Form handling
patterns
- serialization.md - What can/can't
serialize
- error-redirect-handling.md -
fail/redirect/error
Notes
- Server load → universal load via
dataparam | ALWAYS
throw redirect()/error()
- No class instances/functions from server load (not serializable)
- Last verified: 2025-01-11
<!-- PROGRESSIVE DISCLOSURE GUIDELINES:
- Keep this file ~50 lines total (max ~150 lines)
- Use 1-2 code blocks only (recommend 1)
- Keep description <200 chars for Level 1 efficiency
- Move detailed docs to references/ for Level 3 loading
- This is Level 2 - quick reference ONLY, not a manual
LLM WORKFLOW (when editing this file): 1. Write/edit SKILL.md 2. Format (if formatter available) 3. Run: claude-skills-cli validate <path> 4. If multi-line description warning: run claude-skills-cli doctor <path> 5. Validate again to confirm -->
Sveltekit Data Flow
SvelteKit data flow guidance. Use for load functions, form actions, and server/client data. Covers +page.server.ts vs +page.ts, serialization, fail(), redirect(), error().
Structure
SKILL.md- Main skill instructionsreferences/- Detailed documentation loaded as neededscripts/- Executable code for deterministic operationsassets/- Templates, images, or other resources
Usage
This skill is automatically discovered by Claude when relevant to the task.
Error & Redirect Handling: fail(), redirect(), error()
The Three Ways to Handle Failures
| Function | When | Must Throw? | Use Case |
|---|---|---|---|
fail() | Validation error | No (return) | Form validation errors |
redirect() | Navigate user | YES | After successful action |
error() | Fatal error | YES | Unauthorized, not found, server error |
fail() - Validation Errors
Return (don't throw) from form actions to show validation errors:
import { fail } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email');
// Validation failed - return fail()
if (!email || !email.includes('@')) {
return fail(400, {
email,
error: 'Invalid email',
missing: !email,
});
}
// Success - process and maybe redirect
await processEmail(email);
throw redirect(303, '/success');
},
};<!-- +page.svelte -->
<script>
export let form; // Contains fail() return value
</script>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<form method="POST">
<input name="email" value={form?.email ?? ''} />
{#if form?.missing}
<span>Email is required</span>
{/if}
<button>Submit</button>
</form>Key points:
- Return fail() (don't throw)
- Status code (400 = bad request)
- Return validation errors + form data to repopulate fields
- Accessible via
formprop in page component - Page stays on same URL
redirect() - Navigation
Throw redirect() to navigate user to another page:
import { redirect } from '@sveltejs/kit';
export const actions = {
login: async ({ request, cookies }) => {
const data = await request.formData();
const user = await authenticate(data);
if (!user) {
return fail(401, { error: 'Invalid credentials' });
}
cookies.set('session', user.sessionToken, { path: '/' });
// Success - redirect to dashboard
throw redirect(303, '/dashboard'); // MUST throw
},
};Status codes:
303- See Other (recommended for POST → GET redirect)301- Moved Permanently302- Found (temporary redirect)307- Temporary Redirect (preserves method)308- Permanent Redirect (preserves method)
Use 303 for most cases (especially after form submission).
Key points:
- MUST throw (not return)
- Use status 303 for form actions
- Can redirect to external URLs
- Can use relative paths:
throw redirect(303, '..')
error() - Fatal Errors
Throw error() for unrecoverable errors (auth, not found, server error):
import { error } from '@sveltejs/kit';
export const load = async ({ params, locals }) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, params.id),
});
if (!post) {
throw error(404, 'Post not found'); // MUST throw
}
if (post.authorId !== locals.userId) {
throw error(403, 'Forbidden'); // MUST throw
}
return { post };
};Common status codes:
400- Bad Request401- Unauthorized (not logged in)403- Forbidden (logged in but no permission)404- Not Found500- Internal Server Error
With custom error page:
// src/routes/posts/[id]/+page.server.ts
import { error } from '@sveltejs/kit';
export const load = async ({ params }) => {
const post = await getPost(params.id);
if (!post) {
throw error(404, {
message: 'Post not found',
postId: params.id,
});
}
return { post };
};<!-- src/routes/posts/[id]/+error.svelte -->
<script>
import { page } from '$app/stores';
</script>
<h1>{$page.status}: {$page.error.message}</h1>
{#if $page.error.postId}
<p>Could not find post with ID: {$page.error.postId}</p>
{/if}Key points:
- MUST throw (not return)
- Renders closest
+error.sveltefile - Accessible via
$page.statusand$page.error - Stops load function execution
- Use for authorization, not found, server errors
Common Mistakes
❌ Not Throwing redirect()
// WRONG
export const actions = {
default: async () => {
redirect(303, '/home'); // DOESN'T WORK
},
};
// RIGHT
export const actions = {
default: async () => {
throw redirect(303, '/home'); // MUST throw
},
};❌ Not Throwing error()
// WRONG
export const load = async () => {
error(404, 'Not found'); // DOESN'T WORK
};
// RIGHT
export const load = async () => {
throw error(404, 'Not found'); // MUST throw
};❌ Throwing fail()
// WRONG
export const actions = {
default: async () => {
throw fail(400, { error: 'Bad' }); // Don't throw
},
};
// RIGHT
export const actions = {
default: async () => {
return fail(400, { error: 'Bad' }); // Return
},
};❌ Catching redirect Without Rethrowing
// WRONG
export const actions = {
default: async () => {
try {
await doSomething();
throw redirect(303, '/success');
} catch (e) {
console.error(e); // Catches redirect - it won't work!
return fail(500, { error: 'Failed' });
}
},
};
// RIGHT
import { isRedirect } from '@sveltejs/kit';
export const actions = {
default: async () => {
try {
await doSomething();
throw redirect(303, '/success');
} catch (e) {
if (isRedirect(e)) throw e; // Rethrow redirect
console.error(e);
return fail(500, { error: 'Failed' });
}
},
};Decision Tree
Problem in form action?
├─ Validation error (show to user) → return fail(400, { errors })
├─ Success (navigate) → throw redirect(303, '/success')
└─ Fatal error (auth, not found) → throw error(403, 'Forbidden')
Problem in load function?
├─ Data not found → throw error(404, 'Not found')
├─ Unauthorized → throw error(401, 'Unauthorized')
├─ Forbidden → throw error(403, 'Forbidden')
└─ Server error → throw error(500, 'Server error')Patterns
Pattern 1: Validate, Process, Redirect
export const actions = {
create: async ({ request }) => {
const data = await request.formData();
// 1. Validate
if (!isValid(data)) {
return fail(400, { errors: getErrors(data) });
}
// 2. Process
const post = await createPost(data);
// 3. Redirect
throw redirect(303, `/posts/${post.id}`);
},
};Pattern 2: Check Auth, Load Data
export const load = async ({ locals, params }) => {
// 1. Check auth
if (!locals.user) {
throw error(401, 'Please log in');
}
// 2. Load data
const post = await getPost(params.id);
if (!post) {
throw error(404, 'Post not found');
}
// 3. Check permission
if (post.authorId !== locals.user.id) {
throw error(403, 'Not your post');
}
return { post };
};Pattern 3: Conditional Redirect
export const load = async ({ locals }) => {
// Redirect if already logged in
if (locals.user) {
throw redirect(303, '/dashboard');
}
return {}; // Show login page
};Summary Table
| fail() | redirect() | error() | |
|---|---|---|---|
| Throw or return? | Return | Throw | Throw |
| Use in | Form actions | Actions & load | Actions & load |
| Purpose | Validation errors | Navigate | Fatal errors |
| Status codes | 400-499 | 301-308 | 400-599 |
| Accessible via | form prop | N/A (navigates) | +error.svelte |
| Stays on page? | Yes | No (navigates) | No (error page) |
| Example | Invalid email | After save | Not found |
Quick Checklist
- ✅ Using
return fail()for validation errors? - ✅ Using
throw redirect()(not return) after success? - ✅ Using
throw error()(not return) for fatal errors? - ✅ Not catching redirects/errors without rethrowing?
- ✅ Using status code 303 for POST redirects?
Form Actions
Basic Pattern
Form actions live in +page.server.ts and handle form submissions:
// +page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
if (!email) {
return fail(400, { email, missing: true });
}
await login(email, password);
throw redirect(303, '/dashboard');
},
};<!-- +page.svelte -->
<script>
export let form; // Contains return value from action
</script>
<form method="POST">
<input name="email" value={form?.email ?? ''} />
{#if form?.missing}
<p class="error">Email is required</p>
{/if}
<button>Login</button>
</form>Named Actions
export const actions: Actions = {
login: async ({ request }) => {
// Handle login
},
register: async ({ request }) => {
// Handle registration
},
};<form method="POST" action="?/login">...</form>
<form method="POST" action="?/register">...</form>Return Values
Option 1: fail() - Show Error to User
import { fail } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
if (!isValid(data)) {
return fail(400, {
error: 'Invalid data',
fields: Object.fromEntries(data), // Preserve input
});
}
// Success - no return needed
},
};Option 2: redirect() - Navigate After Success
import { redirect } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
await processForm(request);
throw redirect(303, '/success'); // MUST throw
},
};Option 3: error() - Fatal Error
import { error } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const user = await getCurrentUser();
if (!user) {
throw error(401, 'Unauthorized'); // MUST throw
}
// Process...
},
};Progressive Enhancement
Form works without JavaScript:
<script>
import { enhance } from '$app/forms';
</script>
<form method="POST" use:enhance>
<!-- Works with or without JS -->
</form>With custom handling:
<form
method="POST"
use:enhance={({ formData, cancel }) => {
// Before submit
formData.append('timestamp', Date.now().toString());
return async ({ result, update }) => {
// After response
if (result.type === 'success') {
await update(); // Update form prop
}
};
}}
>
...
</form>Validation Pattern
import { fail } from '@sveltejs/kit';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const rawData = {
email: data.get('email'),
password: data.get('password'),
};
const result = schema.safeParse(rawData);
if (!result.success) {
return fail(400, {
errors: result.error.flatten().fieldErrors,
data: rawData,
});
}
// result.data is validated
await createUser(result.data);
throw redirect(303, '/welcome');
},
};Common Mistakes
❌ Not Throwing redirect/error
// WRONG
export const actions = {
default: async () => {
redirect(303, '/home'); // DOESN'T WORK - must throw
},
};
// RIGHT
export const actions = {
default: async () => {
throw redirect(303, '/home');
},
};❌ Catching redirect Without Rethrowing
// WRONG
export const actions = {
default: async () => {
try {
await doSomething();
throw redirect(303, '/success');
} catch (e) {
console.error(e); // Catches redirect!
}
},
};
// RIGHT
export const actions = {
default: async () => {
try {
await doSomething();
throw redirect(303, '/success');
} catch (e) {
if (e instanceof Redirect) throw e; // Rethrow
console.error(e);
}
},
};❌ Returning non-serializable data
// WRONG
export const actions = {
default: async () => {
return { date: new Date() }; // Date is not serializable
},
};
// RIGHT
export const actions = {
default: async () => {
return { date: new Date().toISOString() };
},
};File Upload
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const file = data.get('file') as File;
if (!file || file.size === 0) {
return fail(400, { error: 'No file uploaded' });
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
await saveFile(buffer, file.name);
throw redirect(303, '/uploads');
},
};<form method="POST" enctype="multipart/form-data">
<input type="file" name="file" required />
<button>Upload</button>
</form>Form Prop in Page
<script>
export let data; // From load function
export let form; // From action return value
</script>
{#if form?.error}
<div class="error">{form.error}</div>
{/if}
{#if form?.success}
<div class="success">Success!</div>
{/if}
<form method="POST">
<input name="email" value={form?.email ?? data.email ?? ''} />
{#if form?.errors?.email}
<p>{form.errors.email}</p>
{/if}
<button>Submit</button>
</form>Key Rules
1. ✅ Actions must be in +page.server.ts (not +page.ts) 2. ✅ ALWAYS throw redirect() and error() (not return) 3. ✅ Return fail() for validation errors 4. ✅ Return only serializable data 5. ✅ Don't catch redirects/errors without rethrowing 6. ✅ Use enhance for progressive enhancement 7. ✅ Access FormData with data.get('fieldName')
Load Functions: Server vs Universal
Decision Matrix
| Need | Use | File |
|---|---|---|
| Database access | Server load | +page.server.ts |
| Secrets/env vars | Server load | +page.server.ts |
| Server-only packages | Server load | +page.server.ts |
| Browser APIs (window, localStorage) | Universal load | +page.ts |
| Client-side fetch | Universal load | +page.ts |
| Runs on both | Universal load | +page.ts |
Server Load (+page.server.ts)
When: Need server-only resources (DB, secrets, server APIs)
Runs: Only on server (never in browser)
// src/routes/profile/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';
export const load: PageServerLoad = async ({ locals, params }) => {
// Access server-only resources
const user = await db.query.users.findFirst({
where: eq(users.id, locals.userId),
});
const posts = await db.query.posts.findMany({
where: eq(posts.authorId, user.id),
});
// Must return serializable data
return {
user: {
id: user.id,
name: user.name,
email: user.email,
},
posts,
};
};Key points:
- Runs only on server
- Can access
$lib/server/*imports - Can use secrets from
envsafely - Return values must be JSON-serializable
- Output is automatically passed to universal load
Universal Load (+page.ts)
When: Need to run on both server and client, or need browser APIs
Runs: Server (during SSR) AND client (during navigation)
// src/routes/dashboard/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ data, fetch }) => {
// `data` comes from +page.server.ts if it exists
const { user } = data;
// Fetch additional data (works on both server and client)
const response = await fetch('/api/stats');
const stats = await response.json();
// Can access browser APIs (but check if in browser first)
const theme =
typeof window !== 'undefined'
? localStorage.getItem('theme')
: null;
return {
user,
stats,
theme,
};
};Key points:
- Runs on both server AND client
- Receives server load output as
dataparameter - Use SvelteKit's
fetch(automatically handles SSR) - Check
typeof window !== 'undefined'for browser APIs - Cannot import from
$lib/server/*
Data Flow
Request → Server Load (+page.server.ts)
↓ (returns { user })
Universal Load (+page.ts)
↓ (receives data: { user }, returns { user, stats })
Page Component (+page.svelte)
↓ (receives data: { user, stats })Example:
// +page.server.ts
export const load = async () => {
return { serverData: 'from server' };
};
// +page.ts
export const load = async ({ data }) => {
console.log(data.serverData); // 'from server'
return { ...data, clientData: 'from universal' };
};
// +page.svelte
<script>
export let data; // { serverData, clientData }
</script>Common Patterns
Pattern 1: Server + Universal
// +page.server.ts - Fetch sensitive data
export const load = async ({ locals }) => {
const user = await getUser(locals.session);
return { user };
};
// +page.ts - Fetch public data
export const load = async ({ data, fetch }) => {
const publicPosts = await fetch('/api/posts').then((r) => r.json());
return { ...data, publicPosts };
};Pattern 2: Conditional Universal Load
// +page.ts
import { browser } from '$app/environment';
export const load = async ({ fetch }) => {
const serverData = await fetch('/api/data').then((r) => r.json());
// Only run in browser
let clientOnlyData = null;
if (browser) {
clientOnlyData = localStorage.getItem('cache');
}
return { serverData, clientOnlyData };
};Pattern 3: Depends for Revalidation
// +page.ts
export const load = async ({ fetch, depends }) => {
depends('app:posts'); // Invalidate with invalidate('app:posts')
const posts = await fetch('/api/posts').then((r) => r.json());
return { posts };
};
// Somewhere else:
import { invalidate } from '$app/navigation';
invalidate('app:posts'); // Re-runs load functionCommon Mistakes
❌ Importing Server Code in Universal Load
// +page.ts - WRONG
import { db } from '$lib/server/database'; // ERROR - can't import server code
export const load = async () => {
const users = await db.query.users.findMany(); // Won't work
return { users };
};Fix: Move to +page.server.ts
❌ Returning Non-Serializable Data
// +page.server.ts - WRONG
export const load = async () => {
const user = await User.findOne(); // Returns class instance
return { user }; // ERROR - class instances aren't serializable
};Fix: Return plain objects
export const load = async () => {
const user = await User.findOne();
return {
user: {
id: user.id,
name: user.name,
email: user.email,
},
};
};❌ Using window/localStorage Without Check
// +page.ts - WRONG
export const load = async () => {
const theme = localStorage.getItem('theme'); // ERROR on server
return { theme };
};Fix: Check for browser
import { browser } from '$app/environment';
export const load = async () => {
const theme = browser ? localStorage.getItem('theme') : 'light';
return { theme };
};TypeScript
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({
locals,
params,
url,
}) => {
// TypeScript knows return type must be serializable
return {
user: {
id: 1,
name: 'Alice',
},
};
};import type { PageLoad } from './$types';
export const load: PageLoad = async ({ data, fetch, params }) => {
// `data` is typed from +page.server.ts return type
return {
...data,
extra: 'data',
};
};When to Use Which
Use Server Load when:
- Need database access
- Need secrets/environment variables
- Need server-only npm packages
- Need to hide implementation details
Use Universal Load when:
- Need browser APIs (localStorage, window)
- Need to fetch public APIs (works on both)
- Want client-side navigation without server round-trip
- Data is public and doesn't need server resources
Use Both when:
- Server load fetches sensitive data
- Universal load fetches public data or adds client-side enhancements
Serialization: What Can/Can't Be Returned
The Rule
Server load functions and form actions must return JSON-serializable data.
Data travels from server → client as JSON. Non-JSON types break.
✅ Serializable (Safe)
| Type | Example | Notes |
|---|---|---|
| String | "hello" | ✅ |
| Number | 42, 3.14 | ✅ |
| Boolean | true, false | ✅ |
| null | null | ✅ |
| Array | [1, 2, 3] | ✅ |
| Plain Object | { name: 'Alice' } | ✅ |
| Nested | { user: { posts: [] } } | ✅ If all values are serializable |
❌ NOT Serializable (Breaks)
| Type | Example | Why | Fix |
|---|---|---|---|
| Date | new Date() | Becomes string | Use .toISOString() |
| undefined | undefined | Removed from JSON | Use null |
| Function | () => {} | Can't serialize | Remove or convert to data |
| Class instance | new User() | Only serializes properties | Convert to plain object |
| Map | new Map() | Becomes {} | Convert to object: Object.fromEntries(map) |
| Set | new Set() | Becomes {} | Convert to array: Array.from(set) |
| BigInt | 123n | Error | Convert to string |
| Symbol | Symbol('id') | Removed | Don't use |
| RegExp | /test/ | Becomes {} | Convert to string |
| Error | new Error() | Loses stack | Extract message/code |
Examples
❌ Wrong: Returning Date
// +page.server.ts - WRONG
export const load = async () => {
return {
createdAt: new Date(), // Serializes as string, not Date object
};
};<!-- +page.svelte -->
<script>
export let data;
console.log(data.createdAt); // String, not Date
console.log(data.createdAt.getTime()); // ERROR - not a Date
</script>✅ Right: Convert Date to ISO String
// +page.server.ts - RIGHT
export const load = async () => {
const user = await db.users.findFirst();
return {
user: {
id: user.id,
name: user.name,
createdAt: user.createdAt.toISOString(), // Convert to string
},
};
};<!-- +page.svelte -->
<script>
export let data;
const createdAt = new Date(data.user.createdAt); // Parse back to Date
</script>❌ Wrong: Returning Class Instance
// +page.server.ts - WRONG
class User {
constructor(
public id: number,
public name: string,
) {}
getDisplayName() {
return `User: ${this.name}`;
}
}
export const load = async () => {
const user = new User(1, 'Alice');
return { user }; // Methods are lost during serialization
};<script>
export let data;
console.log(data.user.getDisplayName()); // ERROR - method doesn't exist
</script>✅ Right: Return Plain Object
// +page.server.ts - RIGHT
export const load = async () => {
const user = await db.users.findFirst();
return {
user: {
id: user.id,
name: user.name,
email: user.email,
// Only plain data, no methods
},
};
};❌ Wrong: undefined Values
// +page.server.ts - WRONG
export const load = async () => {
return {
name: 'Alice',
email: undefined, // Removed during JSON.stringify
};
};<script>
export let data;
console.log('email' in data); // false - key is missing!
</script>✅ Right: Use null
// +page.server.ts - RIGHT
export const load = async () => {
return {
name: 'Alice',
email: null, // Preserved
};
};❌ Wrong: Map/Set
// +page.server.ts - WRONG
export const load = async () => {
const tags = new Set(['svelte', 'typescript']);
const metadata = new Map([['version', '1.0']]);
return {
tags, // Becomes {}
metadata, // Becomes {}
};
};✅ Right: Convert to Array/Object
// +page.server.ts - RIGHT
export const load = async () => {
const tags = new Set(['svelte', 'typescript']);
const metadata = new Map([['version', '1.0']]);
return {
tags: Array.from(tags), // ['svelte', 'typescript']
metadata: Object.fromEntries(metadata), // { version: '1.0' }
};
};ORM Returns (Drizzle, Prisma)
Most ORMs return plain objects with Date fields:
// +page.server.ts
export const load = async () => {
const user = await db.query.users.findFirst();
// user = { id: 1, name: 'Alice', createdAt: Date }
return {
user: {
...user,
createdAt: user.createdAt.toISOString(), // Convert Date
},
};
};Or use a helper:
function serialize<T extends Record<string, any>>(obj: T): T {
return JSON.parse(JSON.stringify(obj)); // Forces serialization
}
export const load = async () => {
const user = await db.query.users.findFirst();
return { user: serialize(user) };
};BigInt
// +page.server.ts - WRONG
export const load = async () => {
return {
userId: 123456789012345678901234567890n, // ERROR - can't serialize
};
};
// +page.server.ts - RIGHT
export const load = async () => {
return {
userId: '123456789012345678901234567890', // String
};
};Detecting Serialization Issues
SvelteKit will throw an error if you try to return non-serializable data:
Error: Data returned from `load` while rendering / is not serializable:
- Cannot stringify arbitrary non-POJOsTesting Serialization
const data = { user: new User() };
try {
JSON.parse(JSON.stringify(data));
console.log('✅ Serializable');
} catch (e) {
console.log('❌ Not serializable');
}TypeScript Help
Use satisfies to catch issues at compile time:
import type { PageServerLoad } from './$types';
export const load = (async () => {
return {
user: {
id: 1,
name: 'Alice',
createdAt: new Date(), // TypeScript allows this, but it's problematic
},
};
}) satisfies PageServerLoad;Better: Create a type that only allows primitives:
type Serializable =
| string
| number
| boolean
| null
| { [key: string]: Serializable }
| Serializable[];
export const load: PageServerLoad = async () => {
const data: Serializable = {
user: {
id: 1,
name: 'Alice',
createdAt: new Date().toISOString(), // Must be string
},
};
return data;
};Quick Checklist
Before returning from server load or form action:
1. ✅ All values are string, number, boolean, null, array, or plain object? 2. ✅ No Date objects? (convert to .toISOString()) 3. ✅ No undefined? (use null) 4. ✅ No class instances? (convert to plain objects) 5. ✅ No Map/Set? (convert to object/array) 6. ✅ No functions? 7. ✅ No BigInt? (convert to string)