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:
- The component mounts.
- The dynamic import starts after the component’s first render.
- The
$pagesubscription runs immediately because the$pagestore is already available (it’s a regular store, not lazy). - Inside the subscription we call
goto('/'). At this exact moment the router instance may still beundefinedbecause the root layout hasn’t completed its hydration – the placeholder$pagehas no router attached. - 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?
- Create a new SvelteKit app (
npm init svelte@next my‑app). - Add a lazy store similar to the one above (store that resolves after a
setTimeout). - Create a protected route (
src/routes/protected/+page.svelte) that imports the store dynamically and redirects if a condition fails. - Navigate to
/protecteddirectly 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:
- Wait for the
$pagestore to become “ready” – i.e., wait for the first non‑placeholder value. - Use
afterNavigatefrom$app/navigation– it fires after SvelteKit has finished the navigation cycle. - Wrap
gotoin 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$pagevalue.
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
safeGotowith 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
- Identify the entry point where the store is imported dynamically and a navigation occurs.
- Replace direct
gotocalls with one of the three strategies above (promise guard,afterNavigate, orsafeGoto). - Add a unit test that simulates the store loading after a navigation trigger. Use
@testing-library/sveltewithjestand mockwindow.__sveltekit.routerto beundefinedon the first tick, then defined on the next tick. - Run the integration test in CI with
npm run test. The test should assert that no uncaught promise error bubbles up. - 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.

