Published 12/5/2025 · 6 min read
Lesson 18: Error Handling
Things go wrong. Users visit pages that don’t exist. APIs fail. Data is invalid. SvelteKit gives you tools to handle all of this gracefully.
Expected Errors
Use the error() helper for expected errors — things you anticipate and want to handle:
// src/routes/blog/[slug]/+page.server.js
import { error } from "@sveltejs/kit";
export async function load({ params }) {
const post = await db.getPost(params.slug);
if (!post) {
throw error(404, "Post not found");
}
return { post };
}
This renders your error page with status 404.
The Error Page
Create +error.svelte to customize error display:
<!-- src/routes/+error.svelte -->
<script>
import { page } from '$app/stores'
</script>
<div class="error-page">
<h1>{$page.status}</h1>
<p>{$page.error?.message}</p>
{#if $page.status === 404}
<p>The page you're looking for doesn't exist.</p>
<a href="/">Go home</a>
{:else}
<p>Something went wrong. Please try again.</p>
<button onclick={() => location.reload()}>Retry</button>
{/if}
</div>
<style>
.error-page {
text-align: center;
padding: 4rem 2rem;
}
h1 {
font-size: 4rem;
margin: 0;
color: #e53e3e;
}
</style>
Error Page Hierarchy
Error pages follow the layout hierarchy. You can have different error pages for different sections:
src/routes/
├── +error.svelte # Default error page
├── +layout.svelte
├── +page.svelte
├── admin/
│ ├── +error.svelte # Admin-specific errors
│ ├── +layout.svelte
│ └── +page.svelte
└── api/
└── [...]/+server.js # API errors return JSON, not HTML
The nearest +error.svelte up the tree handles the error.
Rich Error Objects
Pass more data in errors:
import { error } from "@sveltejs/kit";
throw error(404, {
message: "Post not found",
code: "POST_NOT_FOUND",
postId: params.id,
});
Access it in your error page:
<script>
import { page } from '$app/stores'
</script>
{#if $page.error?.code === 'POST_NOT_FOUND'}
<p>We couldn't find post {$page.error.postId}</p>
{/if}
Unexpected Errors
Unexpected errors are unhandled exceptions. SvelteKit catches them and shows your error page, but the details are hidden in production (to avoid leaking sensitive info).
export async function load() {
// This throws an unexpected error
const data = JSON.parse("invalid json");
return { data };
}
In development, you see the full error. In production, users see a generic “Internal Error” message.
The handleError Hook
Catch and process unexpected errors:
// src/hooks.server.js
export function handleError({ error, event, status, message }) {
// Log to your error tracking service
console.error("Unexpected error:", error);
// Send to Sentry, LogRocket, etc.
// Sentry.captureException(error)
// Return what the user should see
return {
message: "Something went wrong",
code: "UNEXPECTED_ERROR",
};
}
This runs for both server errors and client errors (with hooks.client.js).
Form Action Errors
Return errors from form actions without throwing:
import { fail } from "@sveltejs/kit";
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get("email");
if (!email) {
return fail(400, {
error: "Email is required",
values: { email },
});
}
// ...
},
};
fail() returns an error without redirecting to an error page. The form stays on screen with the error message.
API Route Errors
For API routes, return JSON errors:
// src/routes/api/users/[id]/+server.js
import { json, error } from "@sveltejs/kit";
export async function GET({ params }) {
const user = await db.getUser(params.id);
if (!user) {
// Option 1: Throw error (returns HTML error page)
// throw error(404, 'User not found')
// Option 2: Return JSON error (better for APIs)
return json(
{ error: "User not found", code: "USER_NOT_FOUND" },
{ status: 404 }
);
}
return json(user);
}
For pure JSON APIs, prefer returning JSON errors rather than throwing.
Try-Catch in Load Functions
Wrap risky operations:
export async function load({ fetch }) {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw error(response.status, "Failed to load data");
}
return { data: await response.json() };
} catch (e) {
// Re-throw SvelteKit errors
if (e.status) throw e;
// Handle unexpected errors
console.error("Load error:", e);
throw error(500, "Something went wrong");
}
}
Client-Side Error Handling
Handle errors in components:
<script>
import { onMount } from 'svelte'
let data = null
let error = null
let loading = true
onMount(async () => {
try {
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
data = await response.json()
} catch (e) {
error = e.message
} finally {
loading = false
}
})
</script>
{#if loading}
<p>Loading...</p>
{:else if error}
<div class="error">
<p>Error: {error}</p>
<button onclick={() => location.reload()}>Retry</button>
</div>
{:else}
<DataDisplay {data} />
{/if}
Global Error Boundary
For truly unexpected client errors, use handleError in hooks.client.js:
// src/hooks.client.js
export function handleError({ error, event, status, message }) {
// Log to analytics
console.error("Client error:", error);
// Could show a toast notification
// showToast('Something went wrong')
return {
message: "An error occurred",
};
}
Not Found Pages
A special 404 page:
<!-- src/routes/+error.svelte -->
<script>
import { page } from '$app/stores'
</script>
{#if $page.status === 404}
<div class="not-found">
<h1>404</h1>
<p>Page not found</p>
<p>Looking for <code>{$page.url.pathname}</code>?</p>
<a href="/">Back to home</a>
</div>
{:else}
<div class="error">
<h1>Error {$page.status}</h1>
<p>{$page.error?.message || 'Something went wrong'}</p>
</div>
{/if}
Practical Example: Robust Data Loading
// src/routes/dashboard/+page.server.js
import { error, redirect } from "@sveltejs/kit";
export async function load({ locals, fetch }) {
// Auth check
if (!locals.user) {
throw redirect(303, "/login");
}
// Load multiple resources with error handling
const [userResult, statsResult, notificationsResult] =
await Promise.allSettled([
fetch("/api/user/profile").then((r) => r.json()),
fetch("/api/user/stats").then((r) => r.json()),
fetch("/api/user/notifications").then((r) => r.json()),
]);
// User profile is required
if (userResult.status === "rejected") {
throw error(500, "Failed to load profile");
}
// Stats and notifications are optional - use defaults if failed
return {
user: userResult.value,
stats: statsResult.status === "fulfilled" ? statsResult.value : null,
notifications:
notificationsResult.status === "fulfilled"
? notificationsResult.value
: [],
errors: {
stats: statsResult.status === "rejected",
notifications: notificationsResult.status === "rejected",
},
};
}
<!-- src/routes/dashboard/+page.svelte -->
<script>
export let data
</script>
<h1>Welcome, {data.user.name}</h1>
{#if data.errors.stats}
<div class="warning">Stats temporarily unavailable</div>
{:else if data.stats}
<StatsPanel stats={data.stats} />
{/if}
{#if data.errors.notifications}
<div class="warning">Couldn't load notifications</div>
{:else}
<NotificationList items={data.notifications} />
{/if}
Key Takeaways
- Use
error()for expected errors (404, 403, etc.) - Create
+error.sveltefor custom error pages - Error pages follow the layout hierarchy
fail()returns form errors without redirectinghandleErrorhook catches unexpected errors- Return JSON errors from API routes
- Use
Promise.allSettledfor partial failure tolerance - Log errors to tracking services in production
Next: Lesson 19: Deployment
Related Articles
- x402 with SvelteKit: Full-Stack Example
Build a complete SvelteKit application with x402 payments - wallet connection, protected routes, and automatic payment handling.
- Error Handling and Transaction Confirmation
Handle Solana errors gracefully, implement proper confirmation patterns, and build resilient transaction flows.
- Interacting with Programs from Svelte
Build Svelte components that interact with Solana programs - token balances, transfers, and real-time updates.