Error handling in a web application isn't an afterthought—it's a core user experience. SvelteKit gives you a structured approach to surface errors gracefully, from validation failures to unexpected server crashes. Error pages in SvelteKit: a hands-on guide for engineers walks you through the tools and patterns we use to catch problems early, log them safely, and show users a friendly message without exposing internals.
Why error handling matters in SvelteKit
Every application fails. A database connection drops, a third-party API times out, or a user submits invalid input. The difference between a frustrating experience and a professional one is whether you've anticipated these moments and built a recovery path.
SvelteKit's error handling system gives you three main levers: route-level error pages, the error() helper in load functions, and a global hook to log and transform errors before they reach the browser. Together, they form a safety net that keeps your app resilient and your users informed.
Creating +error.svelte for each route
The simplest error pages live at +error.svelte in your route directory. When any error bubbles up from a load function or server action in that route or its children, SvelteKit renders this component instead of your normal page.
Let's say you have a product details page at src/routes/products/[id]/+page.svelte. Create a sibling file src/routes/products/[id]/+error.svelte:
<!-- src/routes/products/[id]/+error.svelte -->
<script>
import { page } from '$app/stores';
</script>
<div class="error-container">
<h1>Product not found</h1>
<p>{$page.error?.message || 'Something went wrong.'}</p>
<a href="/products">Back to products</a>
</div>
<style>
.error-container {
padding: 2rem;
text-align: center;
}
</style>The $page.error store gives you access to the error object. In development, you'll see the full stack trace. In production, only what you've explicitly set will show. You can nest error pages too: +error.svelte at the root level catches errors from any route, while a nested one catches errors only from its subtree.
Throwing errors from load functions with error()
When you detect a problem in a +page.server.js or +layout.server.js load function, use the error() helper. It takes an HTTP status code and a message:
// src/routes/products/[id]/+page.server.js
import { error } from '@sveltejs/kit';
export async function load({ params }) {
const product = await db.products.findById(params.id);
if (!product) {
error(404, 'Product not found');
}
return { product };
}When error() is called, it throws an exception that SvelteKit catches and routes to the nearest +error.svelte. The first argument becomes the HTTP status code (sent to the browser), and the second becomes message on the error object.
You can also pass a custom error object as the second argument:
// More detailed error with context
error(403, {
message: 'Access denied',
reason: 'Your subscription has expired'
});Then in your error page, destructure it:
<script>
import { page } from '$app/stores';
</script>
<h1>{$page.error.message}</h1>
<p>{$page.error.reason}</p>This pattern works well for predictable, domain-specific errors: missing records, permission checks, validation failures. For anything unexpected, SvelteKit's global error handler takes over.
Global error logging with handleError
Every unhandled error—whether thrown in a load function, action, or middleware—passes through the handleError hook. Define it in src/hooks.server.js:
// src/hooks.server.js
export async function handleError({ error, event }) {
// Log to your service
const errorId = crypto.randomUUID();
console.error('Unhandled error', {
id: errorId,
path: event.url.pathname,
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
// Send to monitoring service (Sentry, Datadog, etc.)
// await logToMonitoring({ errorId, error, event });
// Return a safe, user-facing error
return {
message: 'An unexpected error occurred',
errorId
};
}The object you return becomes the value of $page.error. Notice we don't send the original error message or stack to the browser—we only send a generic message and an ID the user can share with support for debugging.
This is where you integrate with monitoring platforms. A real-world pattern we've used in production:
import * as Sentry from '@sentry/sveltekit';
export async function handleError({ error, event }) {
const errorId = event.locals.errorId || crypto.randomUUID();
// Let Sentry capture the full context
Sentry.captureException(error, {
contexts: {
sveltekit: {
route: event.route.id,
url: event.url.href,
method: event.request.method
}
},
tags: {
errorId
}
});
return {
message: 'Something went wrong. Please try again later.',
errorId
};
}This lets your ops team investigate production issues without exposing internals to users.
Avoiding information leakage in error messages
A common mistake is returning too much detail. If you surface database errors, stack traces, or SQL queries to the browser, you give attackers reconnaissance data.
Always follow this rule: log everything server-side, show nothing on the client except what users need to recover.
Bad example:
// Don't do this
export async function load({ params }) {
try {
const order = await db.orders.findById(params.id);
return { order };
} catch (err) {
error(500, err.message); // "Connection refused at 192.168.1.5:5432"
}
}Good example:
// Do this instead
export async function load({ params }) {
try {
const order = await db.orders.findById(params.id);
return { order };
} catch (err) {
// Log the real error
console.error('Database error loading order:', err);
// Return safe message to user
error(500, 'Could not load order. Please contact support.');
}
}Combine this with handleError and you've got a complete picture: servers log everything, users see nothing dangerous, and support has an error ID to correlate logs.
Common mistakes
- Forgetting to set HTTP status codes. Use
error(404, ...)not justerror('...'). The status code is crucial for caching, SEO, and client routing logic. - Leaking sensitive data in error messages. Check your error objects before they reach the browser. A database URL or API key hidden in an exception is a security incident.
- Not using +error.svelte for layout errors. If you want a consistent error page across all routes, place it at
src/routes/+error.svelte. Nested error pages override the root. - Ignoring errors in API routes. Server actions (+page.server.js, +layout.server.js) and API routes both pipe errors through
handleError. Don't assume API endpoints are exempt from error handling. - Over-customizing error objects. Stick to simple shapes:
{ message, statusCode }plus any domain-specific context. Complex nested objects are harder to debug.
Key takeaways
- Use
+error.svelteat any route level to customize how errors are displayed. Errors bubble up to the nearest handler. - Call
error(statusCode, message)in load functions to throw predictable, recoverable errors. The status code matters for HTTP semantics. - Implement
handleErrorinhooks.server.jsto log unexpected errors and transform them into safe, user-facing messages. - Never expose internal details (stack traces, database URLs, API keys) in error messages. Log those server-side; show users only what they need.
- Pair error handling with monitoring (Sentry, Datadog, etc.) so you catch and fix problems before users report them.






Comments
Loading comments…