๐ Svelte 5 State Shadowing
When migrating to or developing with Svelte 5 Runes, assigning class-based or external store singletons to local variables can inadvertently trigger silent rendering crashes due to Svelte 4 legacy compatibility rules.
This document outlines the State Shadowing Bug, its symptoms, the underlying AST interpretation collision, and the canonical fix.
๐ The Bug
In Svelte 5, state is managed via the $state rune. However, Svelte 5 also maintains backwards compatibility with Svelte 4 cross-file reactivity patterns. In Svelte 4, prefixing any variable with $ (e.g., $authStore) automatically compiles down to a .subscribe() listener for a Svelte Store.
If you declare a local variable named state, and subsequently use the Svelte 5 $state rune, Svelte prioritizes the Svelte 4 auto-subscription pattern over the rune macro.
โ Problematic Code
<script lang="ts">
// 1. You import an external class or state object
import { createMaletState } from '../../../stores/createMaletStore.svelte';
// 2. You alias it locally using the exact name "state"
const state = createMaletState;
// 3. Somewhere else in the script, you define local reactivity using the Svelte 5 Rune
let needsUpgrade = $state(false);
</script>
Compilation & Runtime Failure
When the Svelte compiler analyzes the AST (Abstract Syntax Tree), it sees $state(false). Due to the existence of the const state variable, Svelte mistakenly compiles $state into a store subscription against your local variable, expecting it to implement Svelte's readable store contract.
When the component attempts to mount, it throws a fatal, silent runtime exception:
Uncaught Svelte error: store_invalid_shape
`state` is not a store with a `subscribe` method
https://svelte.dev/e/store_invalid_shape
๐ Symptoms in Production
Because the Svelte 5 rendering loop executes synchronously during the component layout phase (especially inside {#if} or {#key} blocks), this exception halts DOM updates exactly at the point the component attempts to mount.
Symptoms include:
- Visual UI freezes where buttons or events silently "do nothing".
- Partial view updates: Components higher in the DOM tree (e.g. progress bars) successfully update, while the problematic component and its siblings downstream halt or fail to swap.
- No explicit build errors generated by
svelte-checkor Vite during compilation.
๐ฌ How We Found It
This bug was discovered using the Frontend Debugging Playbook. The short version:
- Stripped reactive wrappers โ removed
$derivedand{#key}to minimize abstraction layers. - Injected a
console.logdirectly into theonclickhandler of the failing "Review" button. - Opened the browser DevTools console and clicked through the wizard.
- Read the output โ the logs confirmed state was advancing (
step: 5), but Svelte threw a fatalstore_invalid_shapeerror immediately after:
[DEBUG] NEXT clicked: {step: 4, canProceed: true}
Uncaught Svelte error: store_invalid_shape
`state` is not a store with a `subscribe` method
at $state (ReviewLaunch.svelte:18:2)
- Traced the stack โ the
at $state (ReviewLaunch.svelte:18:2)line pointed directly tolet needsUpgrade = $state(false), which Svelte misinterpreted as a store subscription against the localconst statevariable.
For the full step-by-step process (including agent-specific guidance), see the Frontend Debugging Playbook.
โ The Fix
The fix is entirely localized to lexical scoping. You must rename any local proxy or variable alias to prevent shadowing the state identifier.
<script lang="ts">
import { createMaletState } from '../../../stores/createMaletStore.svelte';
// Assign the alias to something other than 'state'
const wizardState = createMaletState;
// The $state rune now evaluates correctly as a macro
let needsUpgrade = $state(false);
</script>
When implementing the CreateMaletState wizard or interacting with core frontend forms, explicitly favor domain-specific variable names such as wizardState, maletConfig, or formContext over generic tokens like state.
Related
- Frontend Debugging Playbook โ The full methodology used to diagnose this bug, generalized for all silent UI failures
- Debugging & Testing โ Backend debugging workflows (curl, GraphQL, MongoDB)
- Onboarding & Malet Creation Architecture โ Component tree and state management for the Malet creation wizard