Back to skills
SkillHub ClubAnalyze Data & AIFrontendBackendData / AI

sveltekit-data-flow

Provides clear guidance on SvelteKit's data flow patterns, specifically when to use server vs universal load functions and how to handle form actions. Includes practical examples for fail(), redirect(), and error() usage with common mistakes highlighted.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
192
Hot score
97
Updated
March 20, 2026
Overall rating
A8.4
Composite score
6.9
Best-practice grade
B75.6

Install command

npx @skill-hub/cli install spences10-svelte-claude-skills-sveltekit-data-flow
sveltekitdata-flowserver-actionsload-functionsform-handling

Repository

spences10/svelte-claude-skills

Skill path: .claude/skills/sveltekit-data-flow

Provides clear guidance on SvelteKit's data flow patterns, specifically when to use server vs universal load functions and how to handle form actions. Includes practical examples for fail(), redirect(), and error() usage with common mistakes highlighted.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Frontend, Backend, Data / AI.

Target audience: SvelteKit developers who need guidance on data flow patterns, particularly those working with server/client data separation and form handling.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: spences10.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install sveltekit-data-flow into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/spences10/svelte-claude-skills before adding sveltekit-data-flow to shared team environments
  • Use sveltekit-data-flow for frontend workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: sveltekit-data-flow
# IMPORTANT: Keep description on ONE line for Claude Code compatibility
# prettier-ignore
description: 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().
---

# 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

```typescript
// +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](references/load-functions.md) - Server vs
  universal
- [form-actions.md](references/form-actions.md) - Form handling
  patterns
- [serialization.md](references/serialization.md) - What can/can't
  serialize
- [error-redirect-handling.md](references/error-redirect-handling.md) -
  fail/redirect/error

## Notes

- Server load → universal load via `data` param | 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
-->


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/load-functions.md

```markdown
# 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)

```typescript
// 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 `env` safely
- 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)

```typescript
// 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 `data` parameter
- 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:**

```typescript
// +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

```typescript
// +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

```typescript
// +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

```typescript
// +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 function
```

## Common Mistakes

### ❌ Importing Server Code in Universal Load

```typescript
// +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

```typescript
// +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

```typescript
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

```typescript
// +page.ts - WRONG
export const load = async () => {
	const theme = localStorage.getItem('theme'); // ERROR on server
	return { theme };
};
```

**Fix:** Check for browser

```typescript
import { browser } from '$app/environment';

export const load = async () => {
	const theme = browser ? localStorage.getItem('theme') : 'light';
	return { theme };
};
```

## TypeScript

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

```typescript
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

```

### references/form-actions.md

```markdown
# Form Actions

## Basic Pattern

Form actions live in `+page.server.ts` and handle form submissions:

```typescript
// +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');
	},
};
```

```svelte
<!-- +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

```typescript
export const actions: Actions = {
	login: async ({ request }) => {
		// Handle login
	},

	register: async ({ request }) => {
		// Handle registration
	},
};
```

```svelte
<form method="POST" action="?/login">...</form>
<form method="POST" action="?/register">...</form>
```

## Return Values

### Option 1: fail() - Show Error to User

```typescript
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

```typescript
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

```typescript
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:

```svelte
<script>
	import { enhance } from '$app/forms';
</script>

<form method="POST" use:enhance>
	<!-- Works with or without JS -->
</form>
```

With custom handling:

```svelte
<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

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
// 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

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

```svelte
<form method="POST" enctype="multipart/form-data">
	<input type="file" name="file" required />
	<button>Upload</button>
</form>
```

## Form Prop in Page

```svelte
<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')`

```

### references/serialization.md

```markdown
# 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

```typescript
// +page.server.ts - WRONG
export const load = async () => {
	return {
		createdAt: new Date(), // Serializes as string, not Date object
	};
};
```

```svelte
<!-- +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

```typescript
// +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
		},
	};
};
```

```svelte
<!-- +page.svelte -->
<script>
	export let data;
	const createdAt = new Date(data.user.createdAt); // Parse back to Date
</script>
```

### ❌ Wrong: Returning Class Instance

```typescript
// +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
};
```

```svelte
<script>
	export let data;
	console.log(data.user.getDisplayName()); // ERROR - method doesn't exist
</script>
```

### ✅ Right: Return Plain Object

```typescript
// +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

```typescript
// +page.server.ts - WRONG
export const load = async () => {
	return {
		name: 'Alice',
		email: undefined, // Removed during JSON.stringify
	};
};
```

```svelte
<script>
	export let data;
	console.log('email' in data); // false - key is missing!
</script>
```

### ✅ Right: Use null

```typescript
// +page.server.ts - RIGHT
export const load = async () => {
	return {
		name: 'Alice',
		email: null, // Preserved
	};
};
```

### ❌ Wrong: Map/Set

```typescript
// +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

```typescript
// +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:

```typescript
// +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:

```typescript
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

```typescript
// +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-POJOs
```

## Testing Serialization

```typescript
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:

```typescript
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:

```typescript
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)

```

### references/error-redirect-handling.md

```markdown
# 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:

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

```svelte
<!-- +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 `form` prop in page component
- Page stays on same URL

## redirect() - Navigation

**Throw** redirect() to navigate user to another page:

```typescript
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 Permanently
- `302` - 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):

```typescript
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 Request
- `401` - Unauthorized (not logged in)
- `403` - Forbidden (logged in but no permission)
- `404` - Not Found
- `500` - Internal Server Error

**With custom error page:**

```typescript
// 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 };
};
```

```svelte
<!-- 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.svelte` file
- Accessible via `$page.status` and `$page.error`
- Stops load function execution
- Use for authorization, not found, server errors

## Common Mistakes

### ❌ Not Throwing redirect()

```typescript
// 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()

```typescript
// 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()

```typescript
// 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

```typescript
// 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

```typescript
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

```typescript
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

```typescript
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?

```

sveltekit-data-flow | SkillHub