Handling Errors with +error.svelte

In a perfect world, code never fails. In the real world, APIs go down, databases timeout, users type wrong URLs, and sessions expire mid-request. SvelteKit has a robust system for handling these situations gracefully with +error.svelte files.

What is an Error Boundary?

An error boundary is a component that catches errors thrown by its descendants and renders fallback UI instead of letting the error propagate up and crash the entire application. If a load function throws, SvelteKit stops rendering that route and looks for the nearest +error.svelte in the route hierarchy to render instead.

The critical word is nearest. SvelteKit doesn’t always fall back to a global error page — it looks for the closest ancestor +error.svelte and renders the error there, while keeping all parent layouts intact. A failing blog post doesn’t take down the blog index. A failing dashboard widget doesn’t knock out the navigation.

This “blast radius” containment is what separates a professional error experience from a broken one. Instead of a white screen that loses the user entirely, an error in one part of your app produces a graceful fallback in exactly that part — the rest of the UI keeps working.

Error handling is a UX problem, not just a technical one

Users will encounter errors. What matters is whether they have a clear path forward when they do. A well-placed error boundary with helpful recovery actions — a search box on 404, a retry button on 500, a login link on 401 — keeps users engaged. A blank screen loses them.


Creating an Error Page

Your +error.svelte file receives error details via the page state:

<!-- src/routes/+error.svelte -->
<script lang="ts">
	import { page } from '$app/state'
</script>

<div class="error-container">
	<h1>{page.status}</h1>
	<p class="message">{page.error?.message}</p>

	<a href="/">Go back home</a>
</div>

<style>
	.error-container {
		text-align: center;
		padding: 4rem 2rem;
	}

	h1 {
		font-size: 6rem;
		margin: 0;
		color: var(--brand);
	}

	.message {
		font-size: 1.25rem;
		color: var(--text-dimmed);
		margin-bottom: 2rem;
	}

	a {
		display: inline-block;
		padding: 0.75rem 1.5rem;
		background: var(--brand);
		color: white;
		text-decoration: none;
		border-radius: 4px;
	}
</style>

Throwing Expected Errors

Sometimes you know something is wrong. For example, if a user requests a blog post that doesn’t exist, that’s a 404 error. You can trigger this manually using the error helper:

// src/routes/blog/[slug]/+page.ts
import { error } from '@sveltejs/kit'
import type { PageLoad } from './$types'

export const load: PageLoad = async ({ params, fetch }) => {
	const response = await fetch(`/api/posts/${params.slug}`)

	if (response.status === 404) {
		// Stop execution and show the error page
		error(404, {
			message: 'Post not found',
			hint: 'The post you are looking for might have been removed.'
		})
	}

	if (!response.ok) {
		error(response.status, 'Failed to load post')
	}

	return {
		post: await response.json()
	}
}

Error Object Shape

The error() function accepts:

  • A status code (400, 404, 500, etc.)
  • A message string OR an object with custom properties
// Simple message
error(404, 'Not found')

// Object with custom properties
error(403, {
	message: 'Access denied',
	hint: 'You need to be logged in to view this page.',
	code: 'AUTH_REQUIRED'
})

Accessing Error Details

In your +error.svelte, access the error via page.error:

<script lang="ts">
	import { page } from '$app/state'
</script>

<h1>Error {page.status}</h1>
<p>{page.error?.message}</p>

{#if page.error?.hint}
	<p class="hint">{page.error.hint}</p>
{/if}

{#if page.status === 404}
	<div class="suggestions">
		<h2>You might be looking for:</h2>
		<ul>
			<li><a href="/blog">Blog</a></li>
			<li><a href="/docs">Documentation</a></li>
			<li><a href="/">Homepage</a></li>
		</ul>
	</div>
{/if}

Cascading Error Pages

Error pages cascade upward through the route hierarchy. SvelteKit looks for the nearest +error.svelte file and renders the error there, inside all the parent layouts above it:

src/routes/
├── +error.svelte Global fallback (inside root layout)
├── +layout.svelte
├── blog/
   ├── +error.svelte Blog-specific errors (inside root layout)
   ├── +layout.svelte
   └── [slug]/
       └── +page.ts If this throws...

If src/routes/blog/[slug]/+page.ts throws an error:

  1. SvelteKit checks src/routes/blog/[slug]/+error.svelte — not found
  2. It checks src/routes/blog/+error.svelte — found, renders this
  3. The root +layout.svelte wraps it, so the user still sees your site navigation

This means the blog error page renders inside the root layout. The user sees the error message, but they still have the site header, footer, and navigation. They are not stranded. They can go somewhere else.

This is why placement matters: a +error.svelte next to a deeply nested page catches only that page’s errors and renders inside all the parent UI. A +error.svelte at the root catches everything but has no parent UI to wrap it. Choose placement based on how much context you want to preserve when something fails.

This allows section-specific error pages that match the section’s identity:

<!-- src/routes/blog/+error.svelte -->
<script lang="ts">
	import { page } from '$app/state'
</script>

<div class="blog-error">
	<h1>Oops! {page.status}</h1>
	<p>{page.error?.message}</p>

	<h2>Recent Posts</h2>
	<!-- Keep the user engaged with other content -->
	<a href="/blog">Browse all posts</a>
</div>

Error Page Layout Context

Error pages are rendered inside their parent layout. This means your header and footer still appear:

Root Layout (+layout.svelte)
├── Header
├── Main
   └── Error Page (+error.svelte) ← Renders here
└── Footer

If you want the error page to replace the entire page (no layout), you need to reset the layout:

src/routes/
├── +layout.svelte
├── +error.svelte Uses root layout
└── +error@.svelte Resets to NO layout (bare page)

Different Error Types

404 - Not Found

// Page doesn't exist
error(404, 'Page not found')

// Resource doesn't exist
error(404, {
	message: 'User not found',
	hint: 'This account may have been deleted.'
})

403 - Forbidden

error(403, {
	message: 'Access denied',
	hint: "You don't have permission to view this resource."
})

401 - Unauthorized

// Usually redirect instead, but sometimes error is appropriate
error(401, 'Please log in to continue')

500 - Server Error

// Don't expose internal details to users
error(500, 'Something went wrong. Please try again later.')

Handling Unexpected Errors

When an unhandled error occurs (a bug in your code), SvelteKit:

  1. Logs the full error to the server console
  2. Shows a generic error to the user (hiding sensitive details)

You can customize this in hooks.server.ts:

// src/hooks.server.ts
import type { HandleServerError } from '@sveltejs/kit'

export const handleError: HandleServerError = async ({ error, event, status, message }) => {
	// Log the full error for debugging
	console.error('Unexpected error:', error)

	// Send to error tracking service
	await reportToSentry(error, {
		url: event.url.pathname,
		userId: event.locals.user?.id
	})

	// Return a safe error message for the user
	return {
		message: 'An unexpected error occurred',
		code: 'INTERNAL_ERROR'
	}
}

Designing Good Error Pages

Include Helpful Actions

<script lang="ts">
	import { page } from '$app/state'

	function goBack() {
		history.back()
	}
</script>

<div class="error-page">
	<h1>{page.status === 404 ? 'Page Not Found' : 'Something Went Wrong'}</h1>
	<p>{page.error?.message}</p>

	<div class="actions">
		<button onclick={goBack}>Go Back</button>
		<a href="/">Home</a>
		<a href="/contact">Report Issue</a>
	</div>
</div>

Status-Specific Content

<script lang="ts">
	import { page } from '$app/state'

	const errorContent = {
		404: {
			title: 'Page Not Found',
			emoji: '🔍',
			suggestion: "The page you're looking for doesn't exist."
		},
		403: {
			title: 'Access Denied',
			emoji: '🔒',
			suggestion: "You don't have permission to view this page."
		},
		500: {
			title: 'Server Error',
			emoji: '💥',
			suggestion: 'Something went wrong on our end. Please try again.'
		}
	}

	let content = $derived(
		errorContent[page.status] ?? {
			title: 'Error',
			emoji: '⚠️',
			suggestion: page.error?.message
		}
	)
</script>

<div class="error-page">
	<span class="emoji">{content.emoji}</span>
	<h1>{content.title}</h1>
	<p>{content.suggestion}</p>
</div>

Retry Logic

For transient errors, offer a retry option:

<script lang="ts">
	import { invalidateAll } from '$app/navigation'
	import { page } from '$app/state'

	let retrying = $state(false)

	async function retry() {
		retrying = true
		await invalidateAll()
		retrying = false
	}
</script>

{#if page.status >= 500}
	<button onclick={retry} disabled={retrying}>
		{retrying ? 'Retrying...' : 'Try Again'}
	</button>
{/if}

Conclusion

Error handling in SvelteKit represents a thoughtful balance between automatic safety nets and explicit control. By providing +error.svelte files at strategic points in your routing hierarchy, you create cascading error boundaries that catch failures and present user-friendly alternatives instead of blank screens or generic browser errors.

The error() helper transforms validation failures and business logic errors into structured error responses that flow naturally through your application’s layout hierarchy.

Mastering error handling means understanding the distinction between expected errors (thrown with error()) and unexpected errors (caught by handleError). Expected errors should provide clear user guidance and recovery paths, while unexpected errors require logging, monitoring, and graceful degradation.

By designing error pages that maintain your application’s look and feel, provide helpful actions, and respect the layout hierarchy, you build applications that fail gracefully and maintain user trust even when things go wrong.

Key Takeaways

  • +error.svelte creates error boundary pages that catch errors in load functions and page components, rendering fallback UI instead of breaking the entire application
  • The error() helper signals structured errors with HTTP status codes and messages: error(404, 'Post not found') returns a 404 response with user-friendly messaging
  • Error pages cascade from nearest boundary - SvelteKit searches up the route tree for the closest +error.svelte, falling back to root if none exist
  • Access error details via page from $app/statepage.status provides the HTTP status code and page.error contains the error message and any custom properties you passed to error()
  • Error pages render inside parent layouts maintaining navigation, headers, and consistent UI even during failures
  • handleError hook logs unexpected errors in hooks.server.ts, enabling error monitoring, sanitization, and logging before errors reach clients
  • Design error pages with recovery actions - search boxes for 404s, retry buttons for 500s, navigation links to help users continue their journey
  • Status-specific content improves UX - differentiate between 404 (not found), 403 (forbidden), 500 (server error) with appropriate messaging and actions

See Also