Svelte Debugging Guide
This guide provides a systematic approach to debugging Svelte applications, with special emphasis on Svelte 5 runes and reactivity patterns.
Common Error Patterns
- Reactivity Not Triggering
Symptoms:
-
UI doesn't update when state changes
-
Computed values are stale
-
Event handlers seem to fire but nothing happens
Root Causes:
<!-- WRONG: Mutating object properties without reassignment (Svelte 4) --> <script> let user = { name: 'John' }; function updateName() { user.name = 'Jane'; // Won't trigger reactivity in some cases } </script>
<!-- CORRECT: Reassign the entire object --> <script> let user = { name: 'John' }; function updateName() { user = { ...user, name: 'Jane' }; // Triggers reactivity } </script>
Svelte 5 Fix with Runes:
<script> let user = $state({ name: 'John' }); function updateName() { user.name = 'Jane'; // Deep reactivity works with $state } </script>
- Runes ($state, $derived, $effect) Issues
Error: "Cannot use runes in .js files"
WRONG: Using runes in regular JS file
utils.js
export const count = $state(0); // ERROR!
CORRECT: Rename to .svelte.js
utils.svelte.js
export const count = $state(0); // Works!
Error: "Class properties not reactive"
// WRONG: $state wrapper on class instance has no effect let instance = $state(new MyClass()); // Properties NOT reactive
// CORRECT: Make class properties reactive internally class MyClass { count = $state(0); name = $state(''); } let instance = new MyClass(); // Properties ARE reactive
Error: "Infinite loop in $derived"
<script> let count = $state(0);
// WRONG: Modifying state inside $derived let doubled = $derived(() => { count++; // ERROR: Causes infinite loop! return count * 2; });
// CORRECT: Pure computation only let doubled = $derived(count * 2); </script>
Error: "Infinite update loop in $effect"
<script> import { untrack } from 'svelte';
let count = $state(0); let log = $state([]);
// WRONG: Reading and writing same state $effect(() => { log.push(count); // Infinite loop! });
// CORRECT: Use untrack() to break dependency $effect(() => { untrack(() => { log.push(count); }); }); </script>
Error: "Cannot export reassigned $state"
// WRONG: Exporting reassigned state // store.svelte.js export let count = $state(0); // ERROR if reassigned
// CORRECT: Wrap in object for mutation export const state = $state({ count: 0 }); // Or use getter/setter let _count = $state(0); export const count = { get value() { return _count; }, set value(v) { _count = v; } };
- Store Subscription Problems (Svelte 4)
Memory Leak: Forgotten Unsubscribe
<script> import { myStore } from './stores'; import { onDestroy } from 'svelte';
// WRONG: No cleanup myStore.subscribe(value => console.log(value));
// CORRECT: Clean up subscription const unsubscribe = myStore.subscribe(value => console.log(value)); onDestroy(unsubscribe);
// BEST: Use auto-subscription $: value = $myStore; // Svelte handles cleanup </script>
Store Not Updating
// WRONG: Mutating store value import { writable } from 'svelte/store'; const items = writable([]); items.update(arr => { arr.push('new item'); // Mutation, may not trigger return arr; });
// CORRECT: Return new array items.update(arr => [...arr, 'new item']);
- SSR Hydration Mismatches
Symptoms:
-
Console warning about hydration mismatch
-
Content flickers on page load
-
Interactive elements don't work initially
Common Causes:
<script> import { browser } from '$app/environment';
// WRONG: Different content server vs client let date = new Date().toLocaleString(); // Different on each render!
// CORRECT: Initialize consistently, update on mount let date = ''; import { onMount } from 'svelte'; onMount(() => { date = new Date().toLocaleString(); });
// OR: Guard browser-only code {#if browser} <BrowserOnlyComponent /> {/if} </script>
LocalStorage Access
<script> import { browser } from '$app/environment';
// WRONG: Accessing localStorage during SSR let theme = localStorage.getItem('theme'); // ERROR on server!
// CORRECT: Guard with browser check let theme = $state('light'); if (browser) { theme = localStorage.getItem('theme') ?? 'light'; } </script>
- Compiler Errors
"'foo' is not defined"
<!-- Check for typos in variable names --> <!-- Check script context vs module context --> <script context="module"> // This runs once, not per component export const sharedValue = 'shared'; </script>
<script> // This runs per component instance let instanceValue = 'instance'; </script>
"Cannot bind to property"
<script> // WRONG: Binding to non-bindable prop let { value } = $props(); </script> <input bind:value /> <!-- Error if parent doesn't use bind: -->
<!-- CORRECT: Declare as bindable --> <script> let { value = $bindable() } = $props(); </script>
Debugging Tools
- Svelte DevTools Browser Extension
Install from Chrome Web Store or Firefox Add-ons. Provides:
-
Component tree visualization
-
Props and state inspection
-
Store value monitoring
-
Event tracking
- The $inspect() Rune (Svelte 5)
<script> let count = $state(0); let user = $state({ name: 'John' });
// Log when values change $inspect(count); // Logs: "count", 0, then changes
// Custom effect with .with() $inspect(user).with(console.trace); // Show stack trace
// Multiple values $inspect(count, user); </script>
- The {@debug} Tag
<script> let items = ['a', 'b', 'c']; let filter = ''; </script>
<!-- Pauses execution with devtools open --> {@debug items, filter}
<!-- Shows in console when values change --> {#each items as item} {@debug item} <li>{item}</li> {/each}
- Console Methods
<script> import { onMount, beforeUpdate, afterUpdate, onDestroy } from 'svelte';
// Track component lifecycle onMount(() => console.log('Component mounted')); beforeUpdate(() => console.log('Before update')); afterUpdate(() => console.log('After update')); onDestroy(() => console.log('Component destroyed'));
// Reactive statement debugging $: console.log('Count changed:', count);
// Object inspection $: console.table(items); $: console.dir(complexObject, { depth: null }); </script>
- Vite Dev Server
Start with verbose logging
npm run dev -- --debug
Clear cache if issues persist
rm -rf node_modules/.vite npm run dev
- svelte-check CLI
Type checking and diagnostics
npx svelte-check
Watch mode
npx svelte-check --watch
With specific tsconfig
npx svelte-check --tsconfig ./tsconfig.json
Output format for CI
npx svelte-check --output human-verbose
- VS Code Debugging
// .vscode/launch.json { "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Debug Svelte", "url": "http://localhost:5173", "webRoot": "${workspaceFolder}", "sourceMaps": true } ] }
Enable breakpoints in .svelte files:
-
Open VS Code settings
-
Search for debug.allowBreakpointsEverywhere
-
Enable the setting
The Four Phases (Svelte-specific)
Phase 1: Gather Information
Check Svelte version
npm list svelte
Check for type errors
npx svelte-check --output human-verbose
Check browser console for warnings/errors
Look for:
- Hydration warnings
- Reactivity warnings
- Unhandled promise rejections
Check component props
$inspect($$props); # Svelte 5
Questions to Answer:
-
Is this a Svelte 4 or Svelte 5 project?
-
Are you using SvelteKit or standalone Svelte?
-
Does the issue occur in dev, build, or both?
-
Is it SSR-related (works in CSR but not SSR)?
Phase 2: Isolate the Problem
<!-- Create minimal reproduction --> <script> // Strip component to minimum code that reproduces issue let count = $state(0);
// Add debugging $inspect(count);
function handleClick() { console.log('Before:', count); count++; console.log('After:', count); } </script>
<button onclick={handleClick}> Count: {count} </button>
Isolation Strategies:
-
Comment out unrelated code
-
Remove external dependencies
-
Test in fresh component
-
Check if issue is component-specific or global
Phase 3: Form Hypothesis and Test
Common Hypotheses:
Symptom Hypothesis Test
No reactivity Missing $state Add $state wrapper
Infinite loop Circular dependency Check $derived/$effect
SSR error Browser API in SSR Add browser guard
Props not reactive Destructured props Use $props() correctly
Store stale Subscription issue Use auto-subscription $
<script> // Hypothesis: Props not updating // Test 1: Log prop changes let { value } = $props(); $effect(() => { console.log('Prop value changed:', value); });
// Test 2: Check if parent is reactive // Add $inspect in parent component </script>
Phase 4: Fix and Verify
Verification Checklist:
-
Issue no longer occurs
-
No new console warnings
-
svelte-check passes
-
Works in both dev and build
-
Works in SSR (if applicable)
-
No memory leaks (check DevTools)
Full verification
npx svelte-check npm run build npm run preview # Test production build
Quick Reference Commands
Diagnostic Commands
Check Svelte/SvelteKit versions
npm list svelte @sveltejs/kit
Run type checking
npx svelte-check
Run type checking with watch
npx svelte-check --watch
Check for outdated packages
npm outdated
Clear caches
rm -rf node_modules/.vite .svelte-kit npm run dev
Common Fixes
Reset node_modules
rm -rf node_modules package-lock.json npm install
Regenerate SvelteKit types
npx svelte-kit sync
Update Svelte to latest
npm update svelte @sveltejs/kit
Check for peer dependency issues
npm ls
Debug Environment Variables
Enable Vite debug mode
DEBUG=vite:* npm run dev
Enable SvelteKit debug mode
DEBUG=kit:* npm run dev
Both
DEBUG=vite:,kit: npm run dev
Svelte 5 Migration Pitfalls
From let to $state
<!-- Svelte 4 --> <script> let count = 0; // Implicitly reactive at top level </script>
<!-- Svelte 5 --> <script> let count = $state(0); // Explicitly reactive </script>
From $: to $derived/$effect
<!-- Svelte 4 --> <script> $: doubled = count * 2; // Reactive derivation $: console.log(count); // Reactive side effect </script>
<!-- Svelte 5 --> <script> let doubled = $derived(count * 2); // Derivation $effect(() => { console.log(count); // Side effect }); </script>
From export let to $props
<!-- Svelte 4 --> <script> export let name = 'default'; export let value; </script>
<!-- Svelte 5 --> <script> let { name = 'default', value } = $props(); </script>
From createEventDispatcher to Callback Props
<!-- Svelte 4 --> <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); function handleClick() { dispatch('click', { data: 'value' }); } </script>
<!-- Svelte 5 --> <script> let { onclick } = $props(); function handleClick() { onclick?.({ data: 'value' }); } </script>
Reactive Collections (Svelte 5)
// Import reactive versions of Map, Set, etc. import { SvelteMap, SvelteSet, SvelteURL } from 'svelte/reactivity';
// Use instead of native collections let items = new SvelteMap(); let tags = new SvelteSet();
// These are deeply reactive items.set('key', { nested: 'value' }); tags.add('new-tag');
Form Handling with tick()
<script> import { tick } from 'svelte';
let value = $state('');
async function submitWithValue(newValue) { value = newValue; await tick(); // Wait for DOM to update form.submit(); // Now form has updated value } </script>
Sources
-
Svelte @debug Documentation
-
SvelteKit Breakpoint Debugging
-
Svelte 5 Migration Guide
-
Svelte 5 States: Avoiding Common Reactivity Traps
-
The Guide to Svelte Runes
-
Introducing Runes
-
Exploring the Magic of Runes in Svelte 5