๐ Frontend Debugging Playbook
A step-by-step methodology for tracking down silent UI bugs in the Mallnline SvelteKit frontend. This playbook is designed for new developers, AI agent workflows, and senior engineers alike โ it provides a universally repeatable process for any situation where "the button does nothing" or "the component just won't render."
Why This Exists
Svelte 5's rune-based reactivity, combined with class-based singleton stores and conditional {#if} rendering, can produce failure modes that are entirely invisible to standard tooling:
svelte-checkreports 0 errors.vite buildsucceeds.- No red text in the terminal.
The bug only surfaces at runtime, inside the browser, when a specific component tries to mount. This playbook teaches you how to find those bugs without guessing.
The Process (5 Steps)
Step 1: Reproduce and Observe
Before touching code, clearly establish what should happen versus what does happen.
Checklist:
- Hard refresh the page (
Ctrl+Shift+R/Cmd+Shift+R) to clear HMR state. - Note what visual changes do occur (e.g., progress bar advances but content doesn't swap).
- Note what visual changes don't occur (e.g., component never appears).
- Check if the same issue happens in an incognito window (rules out stale localStorage/cookies).
What to look for:
A "split" between parts of the UI that update and parts that freeze indicates the rendering pipeline is crashing at a specific component boundary. The last component that successfully rendered is your starting point.
Step 2: Strip Reactive Abstractions
Svelte 5 provides powerful abstractions ($derived, {#key}, $effect), but during debugging, these abstractions can obscure the source of state tracking failures.
Temporarily simplify:
- const currentStep = $derived(state.step);
+ // Use state.step directly in the template during debugging
- {#key currentStep}
- {#if currentStep === 1}
- {/key}
+ {#if state.step === 1}
Why: $derived creates a new reactive signal. If the underlying class proxy isn't being tracked correctly by Svelte's compiler, the $derived wrapper silently swallows the failure. Accessing state.step directly forces Svelte to trace the exact reactive path and exposes any proxy or subscription errors.
Step 3: Inject Console Log Markers
This is the most critical debugging step. Place console.log calls directly inside the event handler that triggers the failing behavior โ not in a $effect, not in a lifecycle hook, but in the exact onclick or onsubmit callback.
Template
<button
type="button"
class="nav-btn next"
onclick={() => {
console.log('[DEBUG] NEXT clicked:', {
step: state.step,
canProceed: state.canProceed
});
state.nextStep();
console.log('[DEBUG] After nextStep:', {
step: state.step
});
}}
>
Continue
</button>
Why Two Logs?
- The first log (
NEXT clicked) proves the event handler fires and captures the pre-mutation state. - The second log (
After nextStep) proves the state mutation executed and captures the post-mutation state.
If both logs print but the UI doesn't update, the state changed but the template isn't tracking it โ a reactivity bug.
If only the first log prints before an error, the state mutation itself is crashing โ a runtime bug.
If neither log prints, the button click isn't reaching your handler โ an event propagation bug (see: type="button" fix below).
Guarding Against `type="submit"` Interference
HTML <button> elements default to type="submit". If a button is inside or near a <form> element, clicking it can trigger an unintended form submission, page reload, or SvelteKit navigation reset.
Always add type="button" to non-form buttons:
<button type="button" onclick={handler}>Click</button>
This is especially important in wizard-style interfaces where navigation buttons are not part of a <form>.
Step 4: Read the Browser Console Output
Open your browser's Developer Console (F12 โ Console tab, or Cmd+Option+J on macOS).
Execute the failing flow. You will see one of three patterns:
Pattern A: State Changes, UI Doesn't
[DEBUG] NEXT clicked: {step: 4, canProceed: true}
[DEBUG] After nextStep: {step: 5}
No errors. The state mutated correctly but the component didn't re-render.
Diagnosis: Svelte's template is not tracking the reactive property. This usually means:
- A
$derivedwrapper is stale or disconnected. - A
{#key}block is using a derived value that isn't updating. - The component uses a local
constreference to a property instead of reading it reactively.
Next action: Check how the template accesses the state. Look for intermediate variables that break the reactive chain.
Pattern B: Svelte Runtime Error After Your Log
[DEBUG] NEXT clicked: {step: 4, canProceed: true}
Uncaught Svelte error: store_invalid_shape
`state` is not a store with a `subscribe` method
https://svelte.dev/e/store_invalid_shape
in <unknown>
in +page.svelte
at $state (ReviewLaunch.svelte:18:2)
Diagnosis: Svelte threw an error during the component mount phase. The at $state (ReviewLaunch.svelte:18:2) trace tells you exactly where.
Next action: Open the file referenced in the stack trace. Search for the rune or store usage that Svelte is complaining about. Common causes:
- Variable name collision with a rune (e.g.,
const state = ...shadows$state()). - Missing import for a store or
$deriveddependency. - A
.svelte.tsmodule not exporting a proper reactive class.
Pattern C: No Logs At All
(nothing appears)
Diagnosis: The onclick handler never fired.
Next action:
- Inspect the button in DevTools Elements tab โ is it
disabled? - Is another element overlapping it (z-index, overlay, modal backdrop)?
- Is it
type="submit"causing a page navigation? Addtype="button". - Is there a CSS
pointer-events: noneancestor?
Step 5: Trace to Root Cause
Once you've identified the failing pattern, use targeted searches to find the root cause.
For Runtime Errors (Pattern B)
- Read the Svelte error URL (e.g.,
https://svelte.dev/e/store_invalid_shape) for the official explanation. - Open the file and line from the stack trace.
- Search the file for the rune or pattern that triggered the error:
grep -n '\$state\|$derived\|\$effect' src/lib/components/create-malet/ReviewLaunch.svelte - Cross-reference any local variable names against Svelte 5's reserved rune identifiers:
state,derived,effect,props,bindable,inspect.
For Reactivity Failures (Pattern A)
- Verify the store/class is using
$state()for its reactive fields (not plain=). - Check that the component accesses the store via property reads (not cached snapshots):
- const step = state.step; // โ One-time snapshot, never updates + // โ Access state.step directly in the template - Check for
Object.freeze()or spread operators that create non-reactive copies.
For AI Agent Workflows
When an agent encounters a "button does nothing" or "component won't render" issue on the Mallnline frontend, it should follow this checklist:
Agent Debugging Checklist
- Never guess โ always inject console logs before attempting code fixes.
- Request a screenshot with DevTools open โ the console output is the single most valuable diagnostic artifact.
- Check for variable shadowing first โ search the failing component for
const state =,const derived =,const effect =, or any variable that matches a Svelte 5 rune name. - Check
type="button"โ verify all non-form buttons have explicittype="button". - Avoid adding
$derivedor{#key}as first-line fixes โ these are workarounds, not diagnostics. They can mask the real bug while appearing to work in some HMR states. - Read the trace, don't guess the file โ if Svelte reports
at $state (ReviewLaunch.svelte:18:2), open line 18 ofReviewLaunch.svelte. Don't search the parent component.
Agent Anti-Patterns
| โ Don't | โ Do Instead |
|---|---|
Wrap everything in {#key} to force re-renders |
Identify why the render failed |
Add $derived wrappers around state reads |
Check if the state itself is reactive |
Assume svelte-check passing means no bugs |
Inject console logs and read browser output |
| Make multiple speculative code changes | Follow the 5-step process sequentially |
| Skip reproduction and go straight to fixing | Reproduce โ observe โ log โ trace โ fix |
Common Svelte 5 Failure Modes
| Failure Mode | Symptom | Root Cause | Fix |
|---|---|---|---|
| Rune Shadowing | store_invalid_shape error on mount |
Local variable named state, derived, or effect collides with rune |
Rename the variable (see State Shadowing) |
| Button Type | Click does nothing, no console output | <button> defaults to type="submit" |
Add type="button" |
| Snapshot State | UI freezes but state is correct in logs | const x = state.value captures once |
Use state.value directly in template |
| HMR Desync | Works after hard refresh, breaks after edits | Vite HMR can't patch class-based stores | Hard refresh (Ctrl+Shift+R) to verify |
| Proxy Trap | TypeError: Cannot read property of undefined |
Spread operator or Object.assign strips the Svelte proxy |
Access nested properties directly |
| Conditional Mount Crash | {#if} branch never renders despite condition being true |
Component in the branch throws during initialization | Check DevTools console for errors from the target component |
Quick Reference: Console Log Templates
Event Handler (click, submit, change)
onclick={() => {
console.log('[DEBUG] ACTION:', {
currentValue: state.someProperty,
relatedFlag: state.someFlag
});
state.doSomething();
console.log('[DEBUG] AFTER:', {
newValue: state.someProperty
});
}}
Lifecycle (onMount)
import { onMount } from 'svelte';
onMount(() => {
console.log('[DEBUG] Component mounted:', {
receivedProps: { /* list props */ },
storeSnapshot: {
step: state.step,
name: state.name
}
});
});
Reactive Effect ($effect)
$effect(() => {
console.log('[DEBUG] Effect triggered:', {
trackedValue: state.step,
timestamp: Date.now()
});
});
โ ๏ธ Clean up debug logs before committing. Use
grep -rn '\[DEBUG\]' src/to find and remove all markers.
Related
- Svelte 5 State Shadowing โ The specific AST collision that inspired this playbook
- Debugging & Testing โ Backend debugging workflows (curl, GraphQL, MongoDB queries)
- E2E Testing Infrastructure โ Playwright patterns for automated wizard testing
- Onboarding & Malet Creation Architecture โ State management and component tree for the Malet creation wizard
- Error Tracking (GlitchTip) โ Self-hosted crash reporting that captures the errors this playbook helps you debug