Developer Docs

๐Ÿ”Ž 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-check reports 0 errors.
  • vite build succeeds.
  • 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 $derived wrapper is stale or disconnected.
  • A {#key} block is using a derived value that isn't updating.
  • The component uses a local const reference 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 $derived dependency.
  • A .svelte.ts module not exporting a proper reactive class.

Pattern C: No Logs At All

(nothing appears)

Diagnosis: The onclick handler never fired.

Next action:

  1. Inspect the button in DevTools Elements tab โ€” is it disabled?
  2. Is another element overlapping it (z-index, overlay, modal backdrop)?
  3. Is it type="submit" causing a page navigation? Add type="button".
  4. Is there a CSS pointer-events: none ancestor?

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)

  1. Read the Svelte error URL (e.g., https://svelte.dev/e/store_invalid_shape) for the official explanation.
  2. Open the file and line from the stack trace.
  3. Search the file for the rune or pattern that triggered the error:
    grep -n '\$state\|$derived\|\$effect' src/lib/components/create-malet/ReviewLaunch.svelte
    
  4. Cross-reference any local variable names against Svelte 5's reserved rune identifiers: state, derived, effect, props, bindable, inspect.

For Reactivity Failures (Pattern A)

  1. Verify the store/class is using $state() for its reactive fields (not plain =).
  2. 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
    
  3. 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

  1. Never guess โ€” always inject console logs before attempting code fixes.
  2. Request a screenshot with DevTools open โ€” the console output is the single most valuable diagnostic artifact.
  3. 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.
  4. Check type="button" โ€” verify all non-form buttons have explicit type="button".
  5. Avoid adding $derived or {#key} as first-line fixes โ€” these are workarounds, not diagnostics. They can mask the real bug while appearing to work in some HMR states.
  6. Read the trace, don't guess the file โ€” if Svelte reports at $state (ReviewLaunch.svelte:18:2), open line 18 of ReviewLaunch.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.