LEARN · DEBUGGING GUIDE

SvelteKit load function errors: root cause analysis and fix patterns

Load functions are the backbone of SvelteKit data fetching. When they break, you get blank pages, 500s, or stale data. Here's exactly what to check.

IntermediateSvelte8 min read

What this usually means

The load function is the entry point for data in SvelteKit, running on the server during SSR and on the client during client-side navigation. Most errors fall into three buckets: (1) calling browser-only APIs (like `window` or `document`) during server execution, (2) returning non-serializable data (e.g., `Date` objects, `Map`, `Set`, functions) that break the serialization step when sending data from server to client, or (3) misusing the load function's context—like trying to access `parent` without awaiting it, or assuming `fetch` is available in the same way as the browser. The error messages are often cryptic because they surface during the hydration mismatch or after the serialization boundary.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Check the terminal/server logs: look for the exact error stack trace from `@sveltejs/kit`—it usually points to the specific load function file (e.g., `src/routes/+page.server.js`).
  • 2Open the browser DevTools Network tab and inspect the `__data.json` request. If it returns a 500, copy the response body—it contains the error message.
  • 3Add a try-catch around your load function's logic and `console.error` the error object to see the full stack trace (SvelteKit may swallow some).
  • 4Check if you're using `window`, `localStorage`, or `document` in load without guarding with `if (typeof window !== 'undefined')`.
  • 5Temporarily replace the load function with `return {}` to isolate if the error is in the load itself or in the component rendering the data.
  • 6Ensure all returned values are JSON-serializable: no `undefined` values (use `null` instead), no `Date` objects (convert to string), no circular references.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • search`src/routes/**/+page.server.js` or `+layout.server.js`—server-side load functions (most common for SSR errors).
  • search`src/routes/**/+page.js` or `+layout.js`—universal load functions (run on both server and client).
  • searchBrowser DevTools Network tab, filter by `__data.json`—this is the endpoint SvelteKit calls to fetch data during client navigation.
  • searchServer logs (e.g., `journalctl -u myapp` or Vercel/Netlify function logs) for raw error output.
  • search`src/hooks.server.js`—if you have a global hook that modifies the `event` object, it might interfere with load function context.
  • search`svelte.config.js`—check adapter settings (e.g., `adapter-node` vs `adapter-vercel`) that affect how load functions run.
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningAccessing `window`, `document`, or other browser globals in a server-side load function (SSR only context).
  • warningReturning `undefined` values in the data object—SvelteKit's serialization throws if any value is `undefined`.
  • warningUsing `await parent()` inside a layout load but not declaring the function `async` (throws 'parent is not a function').
  • warningCalling `fetch` with a relative URL in a universal load function (works on client but not server—needs absolute URL or `event.fetch`).
  • warningMutating the `event` object (e.g., `event.locals.user = ...`) inside a load function (should only use `event.locals` for reading).
  • warningCircular references in the returned data object causing `JSON.stringify` to throw.
  • warningUsing `Date` objects or `Map`/`Set` instances in the returned object—these are not serializable to JSON.
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildWrap browser-only code with `if (typeof window !== 'undefined') { ... }` or move it to `onMount` in the component.
  • buildReplace `undefined` values with `null` or omit the key entirely. Use `JSON.parse(JSON.stringify(data))` as a serialization test.
  • buildConvert `Date` objects to ISO strings (`date.toISOString()`) before returning, then parse back in the component.
  • buildUse `event.fetch` instead of global `fetch` in server load functions to preserve request context and cookies.
  • buildFor universal load functions that need browser APIs, use `+page.js` (not `+page.server.js`) and guard the browser code.
  • buildIf you need parent data, always `const parentData = await parent()` inside an async load function.
  • buildUse structuredClone or a library like `superjson` for complex data, but prefer JSON-safe primitives.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun `npm run build` and look for 'Error: ...' during prerendering—SvelteKit catches load errors at build time for prerendered pages.
  • verifiedVisit the page directly (SSR) and also navigate from another page (CSR). If the error only occurs on one, it's a server/client context issue.
  • verifiedInspect the returned data object in the load function with `console.log(JSON.stringify(data))` to confirm serializability.
  • verifiedUse the SvelteKit dev mode (`npm run dev`) and check both terminal and browser console for error messages.
  • verifiedWrite a unit test for the load function using `@sveltejs/kit` test utilities to validate data shape.
  • verifiedCheck the `__data.json` response in the Network tab for any `null` or unexpected values.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't put `try-catch` that swallows all errors—SvelteKit's error handling is designed to show helpful messages; hiding them makes debugging harder.
  • warningAvoid using global `fetch` in server load functions—always use `event.fetch` to ensure cookies and headers are passed.
  • warningDon't assume `event.url` is available on the client in universal load functions—it's only on the server. Use `event.url` only in server loads.
  • warningNever return `undefined` values—SvelteKit's serialization will throw a cryptic error. Filter them out explicitly.
  • warningDon't mutate `event.locals` inside a load function—it's intended for hooks only. Use load return values to pass data.
  • warningAvoid importing large client-only libraries in server load functions—they'll cause import errors or bundle bloat.
( 07 )War story

The Case of the Vanishing User Session

Full-stack DeveloperSvelteKit 1.5, Node.js 18, PostgreSQL via Prisma, deployed on Vercel

Timeline

  1. 09:15Deploy new feature: user dashboard with session data from load function
  2. 09:23User reports blank page on dashboard after login redirect
  3. 09:30Check Vercel logs: see '500 Internal Error' from /dashboard
  4. 09:35Reproduce locally with `npm run dev` — page works fine
  5. 09:40Inspect Network tab: `__data.json` returns 500 with message 'Cannot read properties of undefined (reading 'id')'
  6. 09:45Check server load function: it calls `event.locals.user.id` — but `event.locals.user` is undefined
  7. 09:50Realize hook `handle` sets `event.locals.user` from session cookie, but only for routes matching `/dashboard/*`; the load function runs before hook for nested routes
  8. 09:55Fix: move session parsing to a separate `handle` that runs for all routes, or add a guard in load function
  9. 10:00Deploy fix, verify dashboard loads correctly

I had just shipped a dashboard feature that reads the current user from a session cookie. The load function in `+page.server.js` accessed `event.locals.user.id` to fetch user-specific data. Locally, everything worked in dev mode because my browser had a valid session. But after deployment, users reported a blank page on the dashboard after login.

I checked Vercel logs and saw a 500 error with the stack trace pointing to `event.locals.user.id`. The issue was that my hook `handle` only set `event.locals.user` for routes matching `/dashboard/*` (I had a condition to avoid unnecessary work). But the load function ran before the hook for nested routes? No, actually the hook runs first, but the condition was wrong: I had `if (event.url.pathname.startsWith('/dashboard'))` which worked, but the load function was in a sub-route `/dashboard/settings` and the hook matched correctly. The real culprit was that the `handle` function had an early return for API routes, and the session parsing was after that return. So for the dashboard page, the hook did run, but I had a bug where `event.locals.user` was set to `undefined` when the session was invalid, and my load function didn't check for that.

The fix was straightforward: add a null check in the load function and redirect to login if user is null. But more importantly, I refactored the hook to always set `event.locals.user` (to null if not authenticated) and moved the session parsing to a dedicated function. I also added a guard in the load function using `if (!event.locals.user) throw redirect(303, '/login')`. This pattern is now my standard: always initialize locals in hooks, and always guard in load functions.

Root cause

Load function assumed `event.locals.user` was always defined, but the hook produced `undefined` for invalid sessions, leading to 'Cannot read property of undefined'.

The fix

Added a null check and redirect in the load function, and refactored the hook to always set `event.locals.user` (to null if not authenticated).

The lesson

Never assume `event.locals` values are defined in load functions. Always guard against null/undefined, especially after authentication hooks. Also, test the failure path (invalid session) not just the happy path.

( 08 )Understanding the load function execution context

SvelteKit load functions run in two contexts: server-side rendering (SSR) and client-side navigation. A `+page.server.js` load runs only on the server, while `+page.js` runs on both server (during SSR) and client (during navigation). This dual context is the source of many bugs.

During SSR, the load function has access to `event.request`, `event.url`, `event.fetch`, `event.locals`, and `event.cookies`. On the client, it only has `event.url` (but with the page's URL), `event.fetch` (the browser's fetch), and `event.params`. Notably, `event.locals` and `event.cookies` are not available on the client. If you need client-specific data, use `+page.js` and guard with `typeof window !== 'undefined'`.

( 09 )Serialization pitfalls and the data boundary

The data returned from a load function is serialized via `devalue` (a more powerful `JSON.stringify`) before being sent to the client. However, `devalue` still cannot handle `undefined` values, circular references, or certain non-POJOs. Common mistakes: returning `Date` objects, `Map`, `Set`, `Error` objects, or functions.

To test serialization, add `JSON.parse(JSON.stringify(await loadFunction(event)))` in a temp route. If it throws, fix the non-serializable values. For `Date`, use `toISOString()`. For `Map`/`Set`, convert to arrays. For `undefined`, omit the key or use `null`.

( 10 )The `fetch` trap: relative URLs and request context

Inside a server load function (`+page.server.js`), `event.fetch` is a wrapper around the global `fetch` that includes the request's cookies and headers. Using the global `fetch` with a relative URL (e.g., `fetch('/api/data')`) will fail because relative URLs are resolved relative to the server's base URL, not the page's URL. Always use `event.fetch` with an absolute URL or a path relative to the server.

In universal load functions (`+page.js`), `event.fetch` works on both server and client, but on the server it behaves like the global fetch (no cookies attached). To include cookies, you must manually pass credentials or use `+page.server.js` instead.

( 11 )Race conditions and async pitfalls

A common bug is forgetting to `await` an asynchronous call inside load, leading to a Promise being returned instead of resolved data. SvelteKit will catch this and throw an error like 'load function must return a promise or an object'. Always ensure your load function either returns an object directly or returns a promise that resolves to an object.

Another issue is calling `await parent()` inside a layout load. If the parent load is also async, the order matters. Incorrectly using `parent()` without `await` yields 'parent is not a function' because it's a function that returns a promise. Also, if you access `parent()` inside a `+page.js` (universal), it will only work if the parent is also a universal load—server loads cannot be awaited from client loads.

( 12 )Debugging with stack traces and source maps

SvelteKit's error handling in production may minify stack traces. To get readable traces, enable source maps in your adapter config (e.g., `adapter({ sourceMap: true })`) and check the server logs. In development, the terminal shows the exact file and line number.

If you see '500 Internal Error' with no details, add a global error handler in `src/hooks.server.js` that logs the error: `export function handleError({ error }) { console.error(error); }`. This will print the full error on the server.

Frequently asked questions

Why does my load function work in dev but fail in production?

Dev mode runs on a single Node.js process with access to all APIs. Production may use serverless functions (e.g., Vercel, Netlify) with different runtime constraints. Common differences: environment variables not set, file system access blocked, or adapter-specific limitations. Check that your adapter supports the features you're using (e.g., `event.platform`). Also, dev mode may not trigger the same serialization errors because it sometimes skips the server-to-client data transfer.

How do I access cookies inside a load function?

Use `event.cookies` in `+page.server.js` or `+layout.server.js`. It provides `.get()`, `.set()`, `.delete()` methods. For universal loads (`+page.js`), you cannot access cookies directly; you must either use a server load or pass cookie data via `event.locals` set in a hook. Remember that `event.cookies` is only available on the server.

What is the difference between `+page.js` and `+page.server.js`?

`+page.server.js` runs only on the server, has access to `event.cookies`, `event.locals`, and `event.platform`. It's ideal for database queries or API calls with secrets. `+page.js` runs on both server (during SSR) and client (during navigation), but with limited context (no cookies or locals). Use `+page.js` for data that doesn't require server secrets and can be fetched from a public API.

How do I handle errors in load functions without crashing the page?

Use SvelteKit's `error` and `redirect` utilities. Throw `error(404, 'Not found')` to show a 404 page, or `redirect(303, '/login')` to redirect. You can also catch errors in the load function and return a partial data object with an error flag, then handle it in the component. Avoid try-catch that swallows errors—let SvelteKit handle them for proper error boundaries.

Why does my load function run twice?

SvelteKit runs load functions on the server during SSR and again on the client during hydration. This is expected. If you see multiple runs on the client, check if you're navigating back to the same page or if the page has multiple layout loads. Use `invalidate()` or `invalidateAll()` to control re-fetching. Also, if you use `fetch` inside a component's `onMount`, it may cause an additional request.