Fix SvelteKit navigation error after lazy‑loaded store

descricao: “Learn how to fix the “Uncaught (in promise) TypeError: Cannot read properties of undefined (reading ‘push’)” error caused by a dynamically imported store in SvelteKit.”

I ran into a weird crash in a production SvelteKit app that was using lazy‑loaded stores for feature flags. The moment a component tried to navigate with $app/navigation – or even the $page store – the console exploded with:

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'push')

Turns out the router instance was undefined because the store was imported after the component rendered, and the first navigation happened before SvelteKit had a chance to initialise the $page subscription. In this post I’ll walk through the exact conditions that trigger the bug, why the usual import { goto } from '$app/navigation' works in most cases, and, most importantly, how to reliably guard against the undefined router when a store is lazy‑loaded.


Why does lazy‑loading a store break SvelteKit navigation?

What does SvelteKit do under the hood when I call goto('/')?

When you call goto (or use the $page store’s update pattern) SvelteKit resolves the navigation through an internal router object that lives on the client‑side window.__sveltekit runtime. The router is created during the first client‑side hydration – essentially when the root +layout.svelte mounts.

If a component accesses $page before that hydration finishes, SvelteKit returns a placeholder store whose subscribe method is a no‑op. The placeholder does not expose the router, so any call that eventually resolves to router.push will hit undefined.

How does a dynamic import of a store interfere with that sequence?

Consider this pattern I used for feature flags:

// src/lib/stores/featureFlags.js
import { readable } from 'svelte/store';

export const featureFlags = readable(null, set => {
  // fetch flags from the API
  fetch('/api/flags')
    .then(r => r.json())
    .then(set)
    .catch(() => set({}));
});

In a component I lazily loaded the store because the flag data is only needed on a rarely‑visited page:

<script>
  let flags;
  // Dynamic import – returns a promise
  import('$lib/stores/featureFlags').then(mod => {
    flags = mod.featureFlags;
  });

  // Attempt navigation based on a flag
  $: if (flags) {
    $page.subscribe(p => {
      if (p.url.pathname === '/admin' && !flags.admin) {
        // redirect to home
        import('$app/navigation').then(nav => nav.goto('/'));
      }
    });
  }
</script>

The problem is subtle:

  1. The component mounts.
  2. The dynamic import starts after the component’s first render.
  3. The $page subscription runs immediately because the $page store is already available (it’s a regular store, not lazy).
  4. Inside the subscription we call goto('/'). At this exact moment the router instance may still be undefined because the root layout hasn’t completed its hydration – the placeholder $page has no router attached.
  5. The call bubbles down to router.push, throwing the TypeError.

In a fully static build this often goes unnoticed because the first navigation usually happens after the page is fully hydrated. In a SPA‑style flow where a lazy‑loaded component is rendered as soon as a route changes, the timing window widens and the bug surfaces.


How can I reproduce the error locally?

What minimal project reproduces the crash?

  1. Create a new SvelteKit app (npm init svelte@next my‑app).
  2. Add a lazy store similar to the one above (store that resolves after a setTimeout).
  3. Create a protected route (src/routes/protected/+page.svelte) that imports the store dynamically and redirects if a condition fails.
  4. Navigate to /protected directly from the home page.

You’ll see the same Cannot read properties of undefined (reading 'push') error in the console, and the navigation never completes.

npm run dev
# Open http://localhost:5173
# Click the link to /protected

If you open the DevTools → Sources and set a breakpoint on the line router.push(...) inside SvelteKit’s internal src/runtime/navigation.js, you’ll notice router is undefined.


What are the safe ways to guard against an undefined router?

Should I avoid dynamic imports altogether?

Not necessarily. Dynamic imports are great for code‑splitting, but you have to delay any navigation logic until the router is ready. There are three practical strategies:

  1. Wait for the $page store to become “ready” – i.e., wait for the first non‑placeholder value.
  2. Use afterNavigate from $app/navigation – it fires after SvelteKit has finished the navigation cycle.
  3. Wrap goto in a tiny helper that checks the internal router (a bit hacky but works for legacy code).

Below I’ll flesh out each approach with concrete code.


How do I wait for the $page store to be ready before navigating?

Can I detect the placeholder $page?

Yes. The placeholder $page store emits a single undefined value before the real store takes over. You can filter that out:

<script>
  import { page } from '$app/stores';
  import { onDestroy } from 'svelte';

  let unsubscribe;
  const ready = new Promise(resolve => {
    unsubscribe = page.subscribe(p => {
      // The real $page always has a `url` object
      if (p && p.url) {
        resolve(p);
      }
    });
  });

  onDestroy(() => unsubscribe);

  // Use the promise later
  async function maybeRedirect() {
    const p = await ready;
    if (p.url.pathname === '/admin') {
      const { goto } = await import('$app/navigation');
      goto('/');
    }
  }

  maybeRedirect();
</script>

Why this works: The promise resolves only after the real $page object is emitted, which guarantees the internal router exists. The dynamic import of the navigation module can stay lazy – it only runs after the guard.

Pro Tip: If you have many components that need this guard, extract the logic into a reusable awaitPageReady() helper that returns the first fully‑initialised $page value.


How can I use afterNavigate to defer navigation until the router is alive?

What does afterNavigate provide?

afterNavigate is a lifecycle hook that SvelteKit calls after it has updated the URL, fetched data, and, crucially, attached the router. It receives a Navigation object with from and to URLs, but you can ignore those if you only need the timing guarantee.

<script>
  import { afterNavigate } from '$app/navigation';
  import { onMount } from 'svelte';

  onMount(() => {
    const off = afterNavigate(() => {
      // Router is definitely ready here
      import('$app/navigation').then(({ goto }) => goto('/'));
    });
    return off; // clean up if component unmounts early
  });
</script>

In the lazy‑store scenario, you can combine the two:

<script>
  import { afterNavigate } from '$app/navigation';
  import { page } from '$app/stores';

  let flags;

  import('$lib/stores/featureFlags').then(mod => {
    flags = mod.featureFlags;
  });

  afterNavigate(() => {
    if (!flags) return; // still loading
    const unsub = page.subscribe(p => {
      if (!flags.admin && p.url.pathname === '/admin') {
        import('$app/navigation').then(({ goto }) => goto('/'));
      }
    });
    return unsub;
  });
</script>

Now the navigation attempt only runs after the router is guaranteed to exist and after the store has resolved.


What if I want a tiny wrapper around goto that never throws?

Can I safely call goto even when the router is missing?

Yes, by checking the internal window.__sveltekit?.router object. This is a bit of an internal API, but it’s stable across SvelteKit 1.x releases because it’s part of the public runtime contract.

// src/lib/utils/safeGoto.js
export async function safeGoto(href, opts = {}) {
  // Dynamically import the real goto to keep code‑splitting
  const { goto } = await import('$app/navigation');

  const router = window?.__sveltekit?.router;
  if (router && typeof router.push === 'function') {
    return goto(href, opts);
  }
  // Fallback: use location.assign – full page reload
  console.warn('Router not ready, falling back to location.assign');
  location.assign(href);
}

Use it exactly like goto:

<script>
  import { safeGoto } from '$lib/utils/safeGoto';
  // inside a reactive block or event handler
  safeGoto('/');
</script>

Pro Tip: Pair safeGoto with a UI‑level loading indicator so the user sees a graceful fallback if a hard reload occurs.


How do I refactor an existing lazy‑store navigation to be bullet‑proof?

Step‑by‑step migration plan

  1. Identify the entry point where the store is imported dynamically and a navigation occurs.
  2. Replace direct goto calls with one of the three strategies above (promise guard, afterNavigate, or safeGoto).
  3. Add a unit test that simulates the store loading after a navigation trigger. Use @testing-library/svelte with jest and mock window.__sveltekit.router to be undefined on the first tick, then defined on the next tick.
  4. Run the integration test in CI with npm run test. The test should assert that no uncaught promise error bubbles up.
  5. Deploy to a staging environment and manually verify that navigating to the lazy‑loaded page works both on a fresh load and after a client‑side transition.

Example before‑and‑after

Before (buggy):

<script>
  import { page } from '$app/stores';
  import { goto } from '$app/navigation';

  import('$lib/stores/featureFlags').then(mod => {
    const flags = mod.featureFlags;
    $: if (flags && !flags.admin) {
      // Immediate navigation – may run before router exists
      goto('/');
    }
  });
</script>

After (using afterNavigate guard):

<script>
  import { afterNavigate } from '$app/navigation';
  import { page } from '$app/stores';

  let flags;
  import('$lib/stores/featureFlags').then(mod => {
    flags = mod.featureFlags;
  });

  afterNavigate(() => {
    if (!flags) return;
    const unsub = page.subscribe(p => {
      if (!flags.admin && p.url.pathname === '/admin') {
        import('$app/navigation').then(({ goto }) => goto('/'));
      }
    });
    return unsub;
  });
</script>

The after‑navigate hook guarantees the router is ready, and the lazy store is now only consulted after it resolves, eliminating the race condition.


FAQ

How can I tell if the $page store I receive is the placeholder or the real one?

The placeholder emits a value where p?.url is undefined. Check if (p && p.url) before using navigation‑related properties.

Does using afterNavigate affect SEO or server‑side rendering?

No. afterNavigate only runs in the browser after hydration. Server‑side rendering continues to render the page normally.

Will safeGoto work in SvelteKit edge adapters (e.g., Vercel Edge Functions)?

Yes, because it falls back to location.assign when the router is missing, which is safe in any browser environment. Edge adapters only affect the server side, not the client runtime.

Is there a built‑in SvelteKit API to check router readiness?

As of SvelteKit 1.x there is no public routerReady flag, but the afterNavigate hook is the officially supported way to know when the router is operational.


Conclusion

The “Cannot read properties of undefined (reading ‘push’)” error after a dynamic store import is a classic race condition between lazy code loading and router initialisation. By explicitly waiting for the $page store to become real, using the afterNavigate lifecycle hook, or wrapping goto in a safe helper, you can guarantee that navigation only happens when the internal router exists. The fix is a few lines of defensive code, but it eliminates a hard‑to‑track crash that can appear sporadically in production.

Remember: lazy loading is powerful, but every asynchronous entry point that touches navigation needs a guard. Once you add that guard, your SvelteKit app will navigate smoothly, even under heavy code‑splitting.


Keep following SpiritCode for more deep‑dive engineering posts on SvelteKit, React, and the rest of the modern frontend stack.