SDK & Framework Guides

SvelteKit handleError: Catch Server and Client Errors

SvelteKit's handleError hook is the single place unexpected errors surface. Learn how it differs from expected errors, and how to report both without leaking data.

When a SvelteKit application crashes in production, you need to catch it—not just for your logs, but to understand what went wrong and fix it before the error reaches more users. The handleError hook is SvelteKit's central mechanism for intercepting errors and turning them into actionable context. This post walks you through how handleError works, why SvelteKit's error model exists, and how to thread errors through to your monitoring system with LightTrace.

By the end, you'll understand the crucial distinction between expected and unexpected errors—the design choice that underpins the entire error-handling model—and how to catch both across your full-stack SvelteKit app.

Expected vs. Unexpected Errors: The Core Design

SvelteKit divides all errors into two categories, and this distinction is why you need handleError in the first place.

Expected errors are thrown with the error() helper imported from @sveltejs/kit. These are intentional: a 404 because a resource doesn't exist, a 403 because the user lacks permission, a validation failure because the form had a typo. When you throw an expected error with error(404, { message: 'Post not found' }), SvelteKit shows that exact message to the user. Crucially, the handleError hook does not run for expected errors. This design choice keeps sensitive data scrubbing simple: expected errors are your responsibility; unexpected errors are SvelteKit's concern.

Unexpected errors are everything else: a null pointer dereference, a database connection that timed out, an external API that returned garbage. SvelteKit treats these as potentially sensitive—they may contain internal details, API keys, or database structure that should never reach the browser. So SvelteKit replaces the real error message with a generic 'Internal Error', sends back HTTP 500, and forwards the real exception to handleError for you to log, report, and act on.

This is not a limitation; it's a deliberate security boundary. It's also why developers are often confused when their error message works in development but disappears in production—the error you threw was not expected, so SvelteKit scrubbed it for safety.

Two handleError Hooks: Server and Client

SvelteKit runs on both the server and the client, and each runtime has its own handleError hook. This separation is core to server-side rendering error tracking: your server hook catches load functions and SSR render errors before they reach the browser, while the client hook handles everything that breaks on the user's machine.

hooks.server.js: Server-Side Errors

Create or open src/hooks.server.js and export a handleError function. This catches errors that occur during server-side load functions, API endpoints, server-side rendering (SSR), and any other server context:

// src/hooks.server.js
export async function handleError({ error, event, status, message }) {
  // error: the exception object
  // event: the RequestEvent (contains route.id, request, url, etc.)
  // status: HTTP status code (500 for unexpected errors)
  // message: the sanitized message shown to the user

  console.error("Server error:", error);
  console.error("Route:", event.route.id);
  console.error("URL:", event.url.pathname);

  return {
    message: "An unexpected error occurred",
    errorId: crypto.randomUUID(),
  };
}

The object you return from handleError becomes the shape of $page.error, available to any error page component downstream.

hooks.client.js: Client-Side Errors

On the browser, create or open src/hooks.client.js with a client-side handleError hook. This runs for errors during client-side navigation, component rendering, event handlers, and client-side hydration mismatches:

// src/hooks.client.js
export async function handleError({ error, event, status, message }) {
  // error: the exception object
  // event: NavigationEvent (contains route, from, to, type)
  // status: HTTP status code (500 for unexpected errors)
  // message: the sanitized message shown to the user

  console.error("Client error:", error);
  console.error("Route:", event.route.id);
  console.error("Navigation type:", event.type);

  return {
    message: "Something went wrong",
  };
}

The key difference: on the server, event is a RequestEvent with request details; on the client, it's a NavigationEvent describing the navigation that failed.

If a hook error occurs during SSR, the server hook runs. If the same code runs on the client and crashes during hydration, the client hook runs. Each hook sees errors from its own runtime.

The Practical Pattern: Error IDs for User Support

Returning a generic message is safe, but it leaves you unable to help users who report "something went wrong." The pattern that wins in production is to return a unique error ID alongside the message, so a user can quote it in a support ticket and you can look it up in your logs:

// src/hooks.server.js
import * as Sentry from "@sentry/sveltekit";

export async function handleError({ error, event, status, message }) {
  const errorId = crypto.randomUUID();

  // Send to your error tracker
  Sentry.captureException(error, {
    tags: {
      errorId,
    },
    contexts: {
      sveltekit: {
        route: event.route.id,
        method: event.request.method,
        url: event.url.pathname,
      },
    },
  });

  // Return safe shape with error ID
  return {
    message: "An unexpected error occurred. Reference ID: " + errorId,
    errorId,
  };
}

Now when a user says "I got error ABC-123-DEF," you search your LightTrace dashboard for that ID and see the exact exception, stack trace, breadcrumbs, and the URL/method/route context that caused it. This pattern scales from hobby projects to teams handling thousands of errors per day.

Reporting to LightTrace with Sentry

To send errors to LightTrace, install the Sentry SDK for SvelteKit and point it at your LightTrace DSN:

npm install @sentry/sveltekit

Initialize Sentry in hooks.server.js:

// src/hooks.server.js
import * as Sentry from "@sentry/sveltekit";

Sentry.init({
  dsn: "https://<key>@light-trace.robomiri.com/1",
  environment: process.env.NODE_ENV,
  release: `app@${process.env.npm_package_version}`,
  tracesSampleRate: 1.0,
});

export const handle = Sentry.sentryHandle();

export async function handleError({ error, event, status, message }) {
  const errorId = crypto.randomUUID();

  Sentry.captureException(error, {
    tags: { errorId },
    contexts: {
      sveltekit: {
        route: event.route.id,
        method: event.request.method,
        url: event.url.pathname,
      },
    },
  });

  return { message: "Error " + errorId, errorId };
}

And initialize Sentry on the client in hooks.client.js:

// src/hooks.client.js
import * as Sentry from "@sentry/sveltekit";

Sentry.init({
  dsn: "https://<key>@light-trace.robomiri.com/1",
  environment: import.meta.env.MODE,
  release: `app@${import.meta.env.VITE_APP_VERSION}`,
  tracesSampleRate: 1.0,
});

export const handle = Sentry.sentryHandle();

export async function handleError({ error, event, status, message }) {
  const errorId = crypto.randomUUID();
  Sentry.captureException(error, { tags: { errorId } });
  return { message: "Error " + errorId, errorId };
}

Every error—server or client—now lands in LightTrace with the full stack trace, breadcrumbs, and context you added.

Set user context early (after login) with Sentry.setUser({ id: user.id, email: user.email }) so every error includes who was affected. Add breadcrumbs before key operations with Sentry.addBreadcrumb() to build a timeline of what the user did before the crash.

Rendering Errors with +error.svelte

When an unexpected error occurs, SvelteKit looks for the nearest +error.svelte component to render. The closest one wins—if you have src/routes/blog/+error.svelte, errors in /blog/* routes render that component. If none exists, SvelteKit walks up the tree to src/routes/+error.svelte.

The component receives $page.error, which is the object you returned from handleError:

<!-- src/routes/+error.svelte -->
<script>
  import { page } from "$app/stores";
</script>

<div class="error-page">
  <h1>Something went wrong</h1>
  <p>{$page.error?.message}</p>
  {#if $page.error?.errorId}
    <p>Reference ID: <code>{$page.error.errorId}</code></p>
    <p>Please share this ID with support.</p>
  {/if}
</div>

<style>
  .error-page {
    padding: 2rem;
  }
</style>

This is the user's last line of defense—a clean, branded error page instead of a blank screen or default 500 error.

What handleError Does NOT Catch

Understanding the boundaries is critical:

  1. Expected errors (thrown with error()) do not trigger handleError. They're shown directly to the user and rendered by an error page, but the hook never runs. This is by design—if you throw error(404), you already know what the user should see.

  2. Redirects (thrown with redirect()) do not trigger handleError. A redirect is a control flow mechanism, not an error.

  3. Errors inside handleError itself are not caught by another handleError. If your hook crashes, it bubbles up as a 500 and may hang or crash the request.

If you need to distinguish between expected and unexpected errors in a try-catch block, use the isHttpError() and isRedirect() helpers imported from @sveltejs/kit:

import { error, isHttpError, isRedirect } from "@sveltejs/kit";

export async function load({ params }) {
  try {
    return await fetchPost(params.id);
  } catch (err) {
    if (isHttpError(err)) {
      // Expected error; rethrow and let SvelteKit handle it
      throw err;
    }
    // Unexpected error; handle or convert
    throw error(500, { message: "Failed to load post" });
  }
}

Source Maps for Client-Side Debugging

In production, your client-side JavaScript is minified, so a raw stack trace points at index-4f9a.js:1:52210 rather than your actual code. Source maps solve this. Enable them in your build:

// vite.config.js
export default {
  build: {
    sourcemap: true,
  },
};

Then upload your source maps to LightTrace as part of your deploy. Without them, the client hook sends minified stack traces; with them, you see the exact line of code that crashed.

Full-Stack Error Handling: Start Here

The handleError hook is the centerpiece of SvelteKit error tracking. Initialize Sentry once in each hook, capture exceptions with context, and thread unique error IDs back to the user. Add breadcrumbs before key operations to build a timeline of what went wrong. Set user context so you know who was affected. Upload source maps so your client-side traces point to your real code, not minified garbage.

This pattern—expected vs unexpected, two separate hooks, error IDs for users—is how you add error tracking to any app. SvelteKit's framework support just makes it cleaner. What makes SvelteKit's error model work is the clarity: expected errors are yours to handle; unexpected errors are yours to monitor. This distinction exists for your security and your sanity.

Start tracking errors in minutes

Set up the Sentry SDK to point at LightTrace, add a handleError hook to your SvelteKit app, and start capturing full-stack errors with automatic context—start free today.

Sources:

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.