Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 068 / 100

Debugging Svelte apps for Svelte & SvelteKit developers

Find problems quickly.

Expona Admin7 min read

Introduction

When a Svelte component behaves unexpectedly or a SvelteKit route fails silently, the instinct to sprinkle console.log statements everywhere is real. But debugging Svelte apps for Svelte & SvelteKit developers has matured significantly, and we now have better tools than ever to find problems quickly without drowning in logs. We'll walk through the practical debugging workflow we use in production: reactive value inspection, browser DevTools integration, source map navigation, and server-side visibility into load functions.

Using $inspect to Log Reactive Values

Svelte 5 introduced $inspect, a built-in rune that logs reactive values whenever they change. This replaces the old pattern of manually logging in reactive blocks, and it's far more ergonomic because the logging happens automatically and persists across re-renders.

Let's say we have a counter component that isn't incrementing as expected:

typescript
<script>
  let count = 0;
  let doubled = $derived(count * 2);

  function increment() {
    count += 1;
  }

  $inspect(count, doubled);
</script>

<button onclick={increment}>
  Count: {count}, Doubled: {doubled}
</button>

Every time count or doubled changes, $inspect logs both values to the console with a label. In the browser DevTools console, you'll see output like Object { count: 1, doubled: 2 } each time the button is clicked. This immediate feedback loop is invaluable for understanding when state actually updates versus when you expect it to.

For more granular control, wrap $inspect in a conditional:

typescript
$inspect.with(console.log)(count, doubled)

You can also use a custom handler to filter or transform the logged data, which is useful in production-like environments where you want to send debugging telemetry to an analytics service rather than polluting the console:

typescript
$inspect.with((obj) => {
  if (import.meta.env.DEV) {
    console.log('DEBUG:', obj);
  }
})(count, doubled);

Leveraging Svelte DevTools Extension

The Svelte DevTools browser extension (available for Chrome and Firefox) provides a dedicated tab in your browser DevTools where you can inspect component hierarchy, reactive state, props, and slot content. This is invaluable for understanding component relationships and tracing where props flow through your application.

To use it effectively, install the extension and open any Svelte app in development mode. You'll see a new "Svelte" tab in DevTools. Click on a component in your app, and the DevTools panel highlights that component in the inspector, showing you:

  • The component's reactive variables and their current values
  • Props passed to the component
  • Derived values and computed state
  • The component's parent and children

This is especially helpful when debugging prop drilling in deeply nested components. You can click through the component tree and watch state flow from parent to child. For example, if a child component isn't receiving an expected prop, you can navigate to the parent in DevTools and confirm whether the prop was actually passed or if the parent's state is stale.

The DevTools extension also logs all component lifecycle events and state changes to a timeline, which helps you understand the order of operations when multiple components mount or update simultaneously.

When you run npm run dev, Svelte's development server generates source maps that map compiled JavaScript back to your original Svelte source files. This means when you set a breakpoint or inspect a value in the browser's debugger, you're working directly with your .svelte files, not the transpiled output.

Open your browser's DevTools (F12 or Cmd+Option+I) and go to the Sources tab. You should see your original Svelte files listed in the file tree. Click on any file and set a breakpoint by clicking the line number. When your code executes and hits that breakpoint, execution pauses and you can:

  • Inspect local and global variables in the current scope
  • Step through code line by line (step over, step into, step out)
  • Watch specific expressions and see their values update as you step
  • Hover over variables in the source to see their values inline

Consider a SvelteKit route that loads data from an API. If the data doesn't appear in your component, you might set a breakpoint in the load function:

typescript
export async function load({ fetch }) {
  const response = await fetch('/api/posts');
  const posts = await response.json();
  
  console.log('Loaded posts:', posts); // Breakpoint here
  return { posts };
}

When the breakpoint hits, you can inspect response, posts, and the fetch parameter. You might discover that the API returned a 401 error response but your code didn't check the response status. Without source maps, you'd be stepping through minified code and it would be nearly impossible to pinpoint the issue.

In production builds, source maps are typically not served to clients for security and performance reasons. However, you can generate them and store them server-side for error reporting services like Sentry, which will display stack traces in your original source code even in production.

Reading Server Logs for Load Functions

SvelteKit load functions run on the server (for both +page.server.js and +layout.server.js files), which means their logs won't appear in the browser console. You need to watch your server process's stdout to see logs from load functions and any server-side errors.

When you run npm run dev, SvelteKit's Vite server prints output directly to your terminal. Any console.log, console.error, or thrown errors in a load function appear in this terminal output:

typescript
// src/routes/+page.server.js
export async function load({ fetch, url }) {
  const query = url.searchParams.get('q');
  console.log('Search query:', query); // This prints to server terminal
  
  if (!query) {
    throw error(400, 'Query parameter required');
  }
  
  const response = await fetch(`/api/search?q=${query}`);
  if (!response.ok) {
    console.error('Search API failed:', response.status);
    throw error(500, 'Search failed');
  }
  
  const results = await response.json();
  return { results };
}

When you navigate to /?q=svelte, check your terminal where you ran npm run dev. You'll see Search query: svelte printed there. If the API call fails, you'll see Search API failed: 500 in the terminal before the error page renders in the browser.

For production deployments, ensure your hosting platform or container orchestration system captures and persists these logs. For example, in Docker, stdout from your Node process is automatically captured by the container runtime. In AWS Lambda or Vercel, logs are available through CloudWatch or the platform's log viewer.

A practical pattern is to use structured logging in server-side code. Instead of raw console.log, use a logger library like pino or winston to emit JSON-formatted logs that include timestamps, severity levels, and request IDs. This makes it much easier to search and correlate logs from multiple requests:

typescript
import pino from 'pino';

const logger = pino();

export async function load({ fetch, url }) {
  const query = url.searchParams.get('q');
  logger.info({ query }, 'Received search request');
  
  const response = await fetch(`/api/search?q=${query}`);
  if (!response.ok) {
    logger.error({ status: response.status }, 'Search API failed');
    throw error(500, 'Search failed');
  }
  
  return { results: await response.json() };
}

Common Mistakes

Forgetting to check the server terminal: Many developers assume all logs come through the browser console and miss critical errors from load functions. Make it a habit to keep your terminal visible while developing.

Over-relying on $inspect in production code: While $inspect is useful during development, leaving it in production builds creates unnecessary console output and can leak sensitive data. Use environment checks or remove it before shipping.

Assuming DevTools breakpoints catch all errors: Some errors (like timing issues or race conditions) only manifest under load. Breakpoints help with deterministic bugs, but for race conditions you often need to add logging and reproduce the issue multiple times.

Ignoring source map configuration in builds: When bundling for production, ensure your build tool (Vite) is configured to generate source maps for your error tracking service. An unminified stack trace is worth its weight in gold when diagnosing production issues.

Key Takeaways

  • $inspect logs reactive values automatically whenever they change, eliminating manual logging boilerplate
  • The Svelte DevTools extension provides a dedicated interface for inspecting component state, props, and hierarchy
  • Source maps in the browser let you set breakpoints and inspect variables directly in your original Svelte source files
  • Server-side logs for load functions appear in the development server's terminal output, not the browser console
  • Structured logging libraries like pino make it easier to search, filter, and correlate logs in production
  • Combine multiple debugging techniques: use $inspect for quick feedback, DevTools for state inspection, breakpoints for step-through debugging, and server logs for full-stack visibility
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles