Error Tracking for Svelte and SvelteKit: Catching Load-Function and Hydration Failures
EngineeringJune 17, 202612 min read

Error Tracking for Svelte and SvelteKit: Catching Load-Function and Hydration Failures

SvelteKit load functions and form actions fail in ways generic SDKs miss. Here's how to catch them with handleError and pageview tracking in one embed.

The form submission worked in dev. Worked in staging. Pushed to production Friday afternoon — because apparently I enjoy stress — and spent Saturday morning staring at a blank checkout page on mobile Safari. No errors in the console. Network tab showed a 200. The page just... didn't render.

Turned out a load function was throwing inside a try-catch that swallowed the error and returned an empty object. Classic SvelteKit footgun. Generic error tracking saw nothing because the error never reached window.onerror. The load function failed silently, the page rendered with undefined data, and my weekend was ruined. (I should've known better. I really should've.)

This tutorial shows you how to set up error tracking that actually catches SvelteKit's failure modes — load function crashes, form action errors, hydration mismatches, and the silent failures that slip through generic JavaScript SDKs. We'll wire it into JustAnalytics so you get errors and pageviews in one dashboard without bolting three different tools together. For teams managing multiple client projects, consider pairing this with VeloCalls for inbound lead tracking — when errors impact conversions, you'll want both data sources. Fair warning: once you see how many errors you've been missing, you might get a little depressed.

What you'll have by the end

A SvelteKit application with:

  • Server-side error tracking via hooks.server.ts that catches load function and form action failures
  • Client-side error tracking via hooks.client.ts that catches hydration errors and navigation failures
  • Automatic pageview tracking with route parameters and timing
  • Source map support so your minified stack traces are readable
  • One under-5KB script instead of separate analytics and error tracking tools

Prerequisites

  • SvelteKit 2.0+ (tested on 2.5.x) with Vite
  • Node.js 18+ for local development
  • A JustAnalytics account — free tier covers 100K events/month, plenty for testing
  • Basic familiarity with SvelteKit's file-based routing and hooks system

If you're on a different framework, we've covered Django middleware and Vue/Nuxt setups separately. (And if you're on Next.js 15, check our App Router guide — the patterns are similar but the implementation differs.)

Step 1: Set up the server-side hook

Create src/hooks.server.ts if it doesn't exist. This is where SvelteKit routes all server-side errors — load functions, form actions, and server endpoint failures.

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

const JUSTANALYTICS_API_KEY = process.env.JUSTANALYTICS_API_KEY;
const JUSTANALYTICS_SITE_ID = process.env.JUSTANALYTICS_SITE_ID;

export const handleError: HandleServerError = async ({ error, event, status, message }) => {
  // Don't track 404s as errors — they're expected behavior
  if (status === 404) {
    return { message: 'Not found' };
  }

  const errorPayload = {
    site_id: JUSTANALYTICS_SITE_ID,
    event: 'exception',
    properties: {
      type: 'server',
      path: event.url.pathname,
      method: event.request.method,
      status_code: status,
      error_message: message,
      error_name: error instanceof Error ? error.name : 'UnknownError',
      stack_trace: error instanceof Error ? error.stack?.slice(0, 5000) : undefined,
      route_id: event.route.id,
      // Include query params for debugging but strip sensitive values
      has_query_params: event.url.search.length > 1,
      user_agent: event.request.headers.get('user-agent')?.slice(0, 200),
    }
  };

  // Fire and forget — don't block the error response
  if (JUSTANALYTICS_API_KEY) {
    fetch('https://api.justanalytics.app/v1/events', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${JUSTANALYTICS_API_KEY}`
      },
      body: JSON.stringify(errorPayload)
    }).catch(() => {
      // Swallow fetch failures — analytics shouldn't break your app
    });
  }

  // Return a safe error message for the client
  return {
    message: 'Something went wrong'
  };
};

The status === 404 check is intentional. A lot of teams track 404s as errors, then complain about noise. Bots probing for /wp-admin and users fat-fingering URLs aren't worth alerting on. Track them separately as pageviews with a not_found: true flag if you care.

The ?.slice(0, 5000) on the stack trace isn't paranoia — I've seen stack traces hit 50KB when recursive functions blow up. That'll exceed most API payload limits and rack up your event quota fast. Ask me how I know. (Hint: it involved a $400 overage email.)

Step 2: Set up the client-side hook

Now the client side. Create src/hooks.client.ts:

// src/hooks.client.ts
import type { HandleClientError } from '@sveltejs/kit';
import { browser } from '$app/environment';

const SITE_ID = import.meta.env.VITE_JUSTANALYTICS_SITE_ID;

export const handleError: HandleClientError = async ({ error, status, message }) => {
  if (!browser || !SITE_ID) {
    return { message: 'Something went wrong' };
  }

  const isHydrationError = error instanceof Error &&
    error.message.toLowerCase().includes('hydration');

  const errorPayload = {
    site_id: SITE_ID,
    event: 'exception',
    properties: {
      type: 'client',
      path: window.location.pathname,
      status_code: status,
      error_message: message,
      error_name: error instanceof Error ? error.name : 'UnknownError',
      stack_trace: error instanceof Error ? error.stack?.slice(0, 5000) : undefined,
      is_hydration_error: isHydrationError,
      url: window.location.href,
      referrer: document.referrer || undefined,
      screen_width: window.innerWidth,
      user_agent: navigator.userAgent.slice(0, 200),
    }
  };

  // Use sendBeacon for reliability during page unloads
  const blob = new Blob([JSON.stringify(errorPayload)], { type: 'application/json' });
  navigator.sendBeacon?.('https://api.justanalytics.app/v1/events/beacon', blob);

  return { message: 'Something went wrong' };
};

Why sendBeacon instead of fetch? Client errors often happen right before navigation — the user clicks away, the page unloads, and your fetch gets cancelled. sendBeacon survives page transitions. If you need to support older browsers, fall back to a synchronous image pixel. But honestly? If someone's running IE11 in 2026, they have bigger problems than your error tracking.

The hydration error flag helps you filter later. Hydration errors mean your server-rendered HTML doesn't match the client-side DOM. Browser extensions injecting elements. Non-deterministic rendering. Third-party scripts modifying the DOM before Svelte mounts. These are annoying. I hate debugging them. But you have to track them.

Step 3: Add automatic pageview tracking

You want pageviews alongside errors. Without them, you're debugging blind — you know a load function crashed but not how many users hit that page per day.

Add this to your root +layout.svelte:

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { page } from '$app/stores';
  import { browser } from '$app/environment';
  import { onMount } from 'svelte';

  const SITE_ID = import.meta.env.VITE_JUSTANALYTICS_SITE_ID;

  let lastPath = '';

  function trackPageview(path: string) {
    if (!browser || !SITE_ID || path === lastPath) return;
    lastPath = path;

    const payload = {
      site_id: SITE_ID,
      event: 'pageview',
      properties: {
        path,
        referrer: document.referrer || undefined,
        title: document.title,
        screen_width: window.innerWidth,
        // Performance timing if available
        load_time_ms: performance.timing
          ? performance.timing.loadEventEnd - performance.timing.navigationStart
          : undefined,
      }
    };

    // sendBeacon for reliability
    const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
    navigator.sendBeacon?.('https://api.justanalytics.app/v1/events/beacon', blob);
  }

  onMount(() => {
    // Track initial pageview
    trackPageview($page.url.pathname);
  });

  // Track client-side navigations
  $: if (browser) {
    trackPageview($page.url.pathname);
  }
</script>

<slot />

The lastPath check prevents double-tracking on initial load. Without it, you'd fire once in onMount and once when the reactive statement runs.

Step 4: Handle load function edge cases

Here's the sneaky part. Not all load function failures trigger handleError. If your load function catches its own errors and returns a fallback, the hooks never fire.

Bad pattern (silent failures):

// +page.server.ts — DON'T DO THIS
export async function load({ fetch }) {
  try {
    const res = await fetch('/api/products');
    return { products: await res.json() };
  } catch (e) {
    return { products: [] }; // Silent failure — no error tracking
  }
}

Better pattern (explicit tracking):

// +page.server.ts
import { trackError } from '$lib/analytics';

export async function load({ fetch, url }) {
  try {
    const res = await fetch('/api/products');
    if (!res.ok) {
      trackError('load_function_api_failure', {
        path: url.pathname,
        api_status: res.status,
        api_url: '/api/products'
      });
      return { products: [], loadFailed: true };
    }
    return { products: await res.json() };
  } catch (e) {
    trackError('load_function_exception', {
      path: url.pathname,
      error: e instanceof Error ? e.message : 'Unknown error'
    });
    return { products: [], loadFailed: true };
  }
}

Create a shared tracking utility at src/lib/analytics.ts:

// src/lib/analytics.ts
const API_KEY = process.env.JUSTANALYTICS_API_KEY;
const SITE_ID = process.env.JUSTANALYTICS_SITE_ID;

export function trackError(eventName: string, properties: Record<string, unknown>) {
  if (!API_KEY) return;

  fetch('https://api.justanalytics.app/v1/events', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({
      site_id: SITE_ID,
      event: eventName,
      properties: {
        ...properties,
        timestamp: new Date().toISOString()
      }
    })
  }).catch(() => {});
}

Now you can call trackError anywhere — load functions, form actions, API routes, wherever you handle errors gracefully but still want visibility. Is it more work than just letting errors bubble? Sure. But debugging production without this is worse.

Step 5: Upload source maps for readable stack traces

Production stack traces are useless without source maps. Minified errors look like:

TypeError: Cannot read properties of undefined
    at o (/_app/immutable/nodes/3.abc123.js:1:2847)

After source maps:

TypeError: Cannot read properties of undefined
    at getProductDetails (src/routes/products/[id]/+page.svelte:47:12)

Add source map upload to your build script:

# package.json script or CI step
vite build && \
curl -X POST https://api.justanalytics.app/v1/sourcemaps \
  -H "Authorization: Bearer $JUSTANALYTICS_API_KEY" \
  -F "release=$(git rev-parse HEAD)" \
  -F "sourcemaps=@.svelte-kit/output/client/_app/immutable"

Tag your events with the same release ID so JustAnalytics knows which source maps to use:

// In your error tracking
properties: {
  // ... other properties
  release: process.env.VITE_GIT_SHA || 'development'
}

Common errors and how to fix them

"Cannot find module '$app/environment'" in hooks.server.ts. You're importing a client-only module on the server. Use import { browser } from '$app/environment' only in hooks.client.ts and .svelte files. For server hooks, just assume you're on the server.

Events fire in dev but not in production. Your environment variables aren't making it to production. Vite requires VITE_ prefix for client-exposed variables. For server-only variables in hooks.server.ts, use process.env or your hosting platform's secrets. Vercel, Cloudflare, and Netlify all have different patterns here. JustEmails can alert your team when production configs drift from dev — useful for catching these mismatches before users do.

Hydration errors flooding your dashboard. Grammarly. Password managers. Ad blockers. They inject elements into your DOM before Svelte mounts, and suddenly your error logs are screaming. Filter by is_hydration_error: true and look for patterns. If it's extension-related, there's not much you can do — you're not going to email users asking them to uninstall Grammarly. Session replay helps here — you can see exactly what the DOM looked like when the mismatch happened.

Form action errors not appearing. Make sure your form actions throw errors rather than returning them. SvelteKit only routes thrown errors through handleError. If you return { error: 'Something failed' }, that's a successful response, not an error. Throw it: throw error(500, 'Something failed').

What this won't fix

Look, I'm not going to pretend this covers everything.

Load functions that hang forever. If your API call never resolves, handleError never fires. Add timeouts to your fetches with AbortController — 10 seconds for API calls, track timeout events separately. I learned this the hard way when a third-party API went down and my app just... waited. Forever. If you're running ad campaigns that depend on these pages, tools like ClickzProtect can pause spend when pages start timing out — preventing wasted ad dollars on broken funnels.

Third-party script errors. Google Tag Manager crashes? Ad SDK throws? That won't route through SvelteKit's error handlers. You'd need a global window.onerror fallback. Personally, I ignore those unless they're breaking my functionality. Not my circus.

Errors that happen before SvelteKit boots. If your server throws during initial request handling (before hooks run), that's infrastructure. Platform-level logging — Vercel Functions logs, Cloudflare Workers logs, whatever your host provides. VeloCards integrates with these platform logs to give you a unified view across multiple cloud providers.

Next steps

You've got SvelteKit error tracking plus pageviews in one integration. Not perfect — nothing is — but better than flying blind.

From here:

  • Add form action tracking for granular conversion data
  • Set up SLO alerting so you know when error rates spike
  • Wire in session replay to see what users did before errors hit
  • Check your observability cost if you're running Datadog or Sentry alongside this — you might be surprised how much overlap there is

If you're managing multiple SvelteKit apps across client accounts, JustBrowser helps keep browser sessions separated while you're testing. For infrastructure monitoring beyond the application layer, DevOS gives you unified developer environments that pair well with app-level observability.

Frequently Asked Questions

Why don't generic JavaScript error tracking SDKs catch SvelteKit load function errors?

Load functions run on the server during SSR and on the client during navigation. Generic browser SDKs only instrument window.onerror and Promise rejections in the browser context — they miss server-side load failures entirely. Even client-side, SvelteKit catches load errors internally and routes them through handleError rather than letting them bubble to window.onerror. You need SvelteKit-aware instrumentation that hooks into both hooks.server.ts and hooks.client.ts to catch everything.

How do I track hydration mismatches without flooding my error logs?

Hydration errors fire when the server-rendered HTML doesn't match what Svelte tries to mount on the client. They're noisy in dev but serious in production. Filter by checking if the error message contains 'hydration' and batch similar errors by page path. Set a sampling rate for dev environments and keep production at 100%. Most teams find 3-5 unique hydration error patterns in prod — once you fix those, the volume drops to near zero.

Can I use this setup with SvelteKit deployed to Cloudflare Workers or edge runtimes?

Yes, with a tweak. Edge runtimes don't support Node.js-specific APIs, so use fetch instead of any Node HTTP client. The JustAnalytics ingest endpoint accepts standard fetch requests. The handleError hooks work the same way — Cloudflare Workers execute hooks.server.ts during edge SSR. Watch your timeout settings since edge cold starts add latency; a 2-second timeout works for most deployments.

Does this approach work with SvelteKit form actions and progressive enhancement?

Yes — and this is one of the things SvelteKit actually gets right. Form action errors flow through the same handleError hook as load function failures. When a form action throws, SvelteKit catches it, calls your handleError, and you track it like any other server error. The difference is you'll want to capture the action name and form data shape (not values — those might contain PII) in your error properties. This gives you visibility into which forms fail and why, even when JavaScript is disabled and forms submit directly to the server.


Try JustAnalytics

All-in-one observability in one under-5KB script: cookieless analytics + error tracking + APM + session replay + uptime + structured logs. Replaces GA4 + Sentry + Datadog + Pingdom + LogRocket. Free tier (100K events/mo), Pro $49/month ($39 annual).

Start free → · AI Command Center MCP

JP
JustAnalytics Platform TeamContributor

Author at JustAnalytics.

Related posts