Svelte provides excellent debugging tools and techniques to help you identify and fix issues in your applications.
The official browser extension provides powerful debugging capabilities:
Installation
Features
Component Tree:
- View component hierarchy
- Inspect component props and state
- See which components are mounted
- Track component parent-child relationships
State Inspector:
- View all reactive state (
$state)
- Inspect derived values (
$derived)
- Monitor state changes in real-time
- Modify state values directly
Event Timeline:
- Track component lifecycle events
- See when effects run
- Monitor prop updates
Enable Svelte DevTools in development mode for the best debugging experience. It automatically detects Svelte applications.
The $inspect Rune
Basic Usage
Log reactive state changes with $inspect:
<script>
let count = $state(0);
let user = $state({ name: 'Alice', age: 30 });
// Logs whenever count or user changes
$inspect(count, user);
</script>
<button onclick={() => count++}>Count: {count}</button>
<button onclick={() => user.age++}>Age: {user.age}</button>
Output:
count: 1
user: { name: 'Alice', age: 31 }
at Component.svelte:5:2
$inspect only works in development mode. It becomes a no-op in production builds.
Deep Reactivity Tracking
$inspect tracks changes deeply in objects and arrays:
<script>
let todos = $state([
{ id: 1, text: 'Learn Svelte', done: false },
{ id: 2, text: 'Build app', done: false }
]);
$inspect(todos);
function toggleTodo(id) {
const todo = todos.find(t => t.id === id);
todo.done = !todo.done; // $inspect logs this mutation
}
</script>
Custom Logging with .with
Provide your own logging function:
<script>
let count = $state(0);
$inspect(count).with((type, value) => {
if (type === 'update') {
console.log(`Count changed to ${value}`);
debugger; // Pause execution on changes
}
});
</script>
Parameters:
type: "init" (first call) or "update" (subsequent changes)
...values: The values passed to $inspect
Tracing Reactive Dependencies
Use $inspect.trace() to debug why effects or derived values re-run:
<script>
let firstName = $state('Alice');
let lastName = $state('Smith');
let age = $state(30);
let greeting = $derived.by(() => {
$inspect.trace('greeting calculation');
return `${firstName} ${lastName} (${age})`;
});
</script>
<input bind:value={firstName} placeholder="First name" />
<input bind:value={lastName} placeholder="Last name" />
<input bind:value={age} type="number" />
<p>{greeting}</p>
Console output when firstName changes:
greeting calculation
Dependencies:
- firstName (changed)
- lastName
- age
$inspect.trace() must be the first statement in an effect or derived function body.
Elements Panel:
- Inspect DOM structure
- View component-generated HTML
- Modify styles in real-time
- Check element accessibility
Console:
<script>
let data = $state({ items: [] });
// Make available in console for debugging
globalThis.debug = { data };
$effect(() => {
console.log('Data updated:', data);
});
</script>
Now in console:
> debug.data
{ items: [...] }
> debug.data.items.push({ id: 4 })
// Triggers reactive updates
Performance Panel:
- Start recording
- Interact with your app (click, scroll, type)
- Stop recording
- Analyze flame graph for slow operations
- Identify expensive component renders
Network Panel:
- Monitor API requests
- Check request/response times
- Debug failed requests
- Inspect headers and payloads
Source Maps
Svelte generates source maps automatically in development:
// vite.config.js
export default {
build: {
sourcemap: true // Enable in production for debugging
}
};
With source maps, you can:
- Set breakpoints in
.svelte files
- Step through original source code
- See original variable names
- Get accurate stack traces
Common Debugging Scenarios
Reactivity Not Working
Problem: Component doesn’t update when state changes
<script>
// ❌ Bad: Not reactive
let count = 0;
function increment() {
count++; // DOM won't update
}
</script>
<button onclick={increment}>{count}</button>
Solution: Use $state
<script>
// ✅ Good: Reactive
let count = $state(0);
function increment() {
count++; // DOM updates
}
</script>
Debug with $inspect.trace:
<script>
let count = $state(0);
$effect(() => {
$inspect.trace('count effect');
console.log('Count:', count);
});
</script>
Props Not Updating
Problem: Child component doesn’t react to prop changes
<!-- Child.svelte -->
<script>
let { value } = $props();
// ❌ Bad: Captures initial value only
let doubled = value * 2;
</script>
<p>{doubled}</p>
Solution: Use $derived for computed props
<script>
let { value } = $props();
// ✅ Good: Recalculates when value changes
let doubled = $derived(value * 2);
</script>
Infinite Loops in Effects
Problem: Effect triggers itself
<script>
let items = $state([]);
$effect(() => {
// ❌ Bad: Modifies its own dependency
items = [...items, Math.random()];
});
</script>
Solution: Use the right tool
<script>
let count = $state(0);
// ✅ For initial setup only
$effect(() => {
const interval = setInterval(() => {
count++; // This is fine - effect doesn't depend on count
}, 1000);
return () => clearInterval(interval);
});
</script>
Debug infinite loops:
<script>
let data = $state([]);
let runCount = 0;
$effect(() => {
console.log('Effect run #', ++runCount);
if (runCount > 10) {
debugger; // Pause after 10 runs
}
// ... effect code
});
</script>
Component Not Rendering
Problem: Component instance doesn’t appear in DOM
Check:
- Import path: Is the component imported correctly?
- Component name: Does it start with a capital letter?
- Conditional rendering: Is it inside a falsy
{#if} block?
- Parent mount: Is the parent component mounted?
<script>
import MyComponent from './MyComponent.svelte'; // Check path
let show = $state(true);
$effect(() => {
console.log('Parent mounted');
});
</script>
{#if show}
<MyComponent /> <!-- Capital letter required -->
{:else}
<p>Component hidden</p>
{/if}
Error Boundaries
Catch and handle component errors gracefully:
<script>
import { ErrorBoundary } from './ErrorBoundary.svelte';
import RiskyComponent from './RiskyComponent.svelte';
</script>
<svelte:boundary onerror={(error) => console.error(error)}>
<RiskyComponent />
{#snippet failed(error)}
<div class="error">
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
</div>
{/snippet}
</svelte:boundary>
Use <svelte:boundary> to prevent errors from crashing your entire app.
Testing and Debugging
Component Tests with Vitest
Write tests that act as debugging documentation:
import { render, screen } from '@testing-library/svelte';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';
test('counter increments', async () => {
const { component } = render(Counter, { props: { initial: 0 } });
const button = screen.getByRole('button');
expect(button).toHaveTextContent('0');
await button.click();
expect(button).toHaveTextContent('1');
});
Debug Mode in Tests
import { render } from '@testing-library/svelte';
import { expect, test } from 'vitest';
import Component from './Component.svelte';
test.only('debug specific test', () => {
const { debug } = render(Component);
debug(); // Prints current DOM
// ... test assertions
});
Logging Best Practices
Structured Logging
<script>
function fetchData() {
console.group('fetchData');
console.time('fetch-duration');
fetch('/api/data')
.then(r => r.json())
.then(data => {
console.log('Data:', data);
console.timeEnd('fetch-duration');
console.groupEnd();
})
.catch(error => {
console.error('Fetch failed:', error);
console.groupEnd();
});
}
</script>
Conditional Logging
<script>
const DEBUG = import.meta.env.DEV;
function log(...args) {
if (DEBUG) console.log(...args);
}
let count = $state(0);
$effect(() => {
log('Count changed:', count);
});
</script>
Development vs Production
Environment Detection
<script>
import { DEV } from 'esm-env';
if (DEV) {
console.log('Development mode');
globalThis.debugApp = {
// Expose debugging utilities
};
}
</script>
Debug Panels
<script>
import { DEV } from 'esm-env';
import DebugPanel from './DebugPanel.svelte';
let showDebug = $state(false);
</script>
{#if DEV}
<button onclick={() => showDebug = !showDebug}>
Toggle Debug Panel
</button>
{#if showDebug}
<DebugPanel />
{/if}
{/if}
Debugging Tips
- Use
$inspect liberally - It’s removed in production automatically
- Enable Svelte DevTools - Essential for understanding component state
- Add
debugger statements - Pause execution at specific points
- Log component lifecycle - Track when components mount/unmount
- Test edge cases - Write tests for problematic scenarios
- Use TypeScript - Catch type errors before runtime
- Check the compiler warnings - Svelte warns about common mistakes
- Profile performance - Use browser tools to find bottlenecks
Common mistakes to avoid:
- Forgetting to use
$state for reactive variables
- Using
$effect instead of $derived for computed values
- Not providing keys in
{#each} blocks
- Modifying props directly (use callbacks or events)
- Ignoring compiler warnings
Additional Resources
The best debugging tool is prevention. Use TypeScript, write tests, and pay attention to compiler warnings to catch issues early.