Reactive Debugging at the Template Level
In the intricate dance of reactive state management, understanding when and why values change can be the difference between swift problem resolution and hours of frustration. Svelte 5’s @debug tag offers a declarative approach to debugging that integrates seamlessly with the framework’s reactive model, providing real-time insights into state mutations as they occur.
Unlike traditional debugging approaches that require manual console.log statements scattered throughout your code or elaborate breakpoint configurations, the @debug tag operates reactively—automatically logging values whenever any tracked dependency changes. This makes it an invaluable tool for understanding complex reactive flows, tracking down elusive bugs, and gaining deeper insight into how your component’s state evolves over time.
Understanding when to employ @debug, what to track, and how to interpret its output transforms it from a simple logging mechanism into a powerful diagnostic tool that can illuminate even the most convoluted reactive patterns.
The Problem
The Debugging Challenge
Consider a scenario where you’re building an e-commerce product configurator. Multiple pieces of state interact to determine the final price, and you’re experiencing unexpected calculation results:
<script>
let basePrice = $state(100)
let quantity = $state(1)
let discountCode = $state('')
let memberDiscount = $state(false)
let subtotal = $derived(basePrice * quantity)
let codeDiscount = $derived(discountCode === 'SAVE20' ? 0.2 : 0)
let membershipDiscount = $derived(memberDiscount ? 0.1 : 0)
let totalDiscount = $derived(codeDiscount + membershipDiscount)
let finalPrice = $derived(subtotal * (1 - totalDiscount))
</script>
<div class="configurator">
<label>
Quantity: <input type="number" bind:value={quantity} min="1" />
</label>
<label>
Discount Code: <input bind:value={discountCode} />
</label>
<label>
<input type="checkbox" bind:checked={memberDiscount} />
Member Discount
</label>
<div class="price">
Final Price: ${finalPrice.toFixed(2)}
</div>
</div> When the price calculation seems incorrect, you need to understand which values are changing and when. Traditional debugging requires inserting multiple console.log statements, removing them later, and potentially missing which reactive chain triggered the update.
The @debug Solution
The @debug tag provides reactive logging that automatically fires whenever tracked values change:
<script>
let basePrice = $state(100)
let quantity = $state(1)
let discountCode = $state('')
let memberDiscount = $state(false)
let subtotal = $derived(basePrice * quantity)
let codeDiscount = $derived(discountCode === 'SAVE20' ? 0.2 : 0)
let membershipDiscount = $derived(memberDiscount ? 0.1 : 0)
let totalDiscount = $derived(codeDiscount + membershipDiscount)
let finalPrice = $derived(subtotal * (1 - totalDiscount))
</script>
{@debug quantity, subtotal, totalDiscount, finalPrice}
<div class="configurator">
<!-- inputs as before -->
</div> Now, whenever any of the tracked values change, you’ll see console output showing their current state. The logging is automatic, reactive, and requires no manual cleanup.
Basic Usage
Single Value Tracking
Track a single reactive value to understand when it changes:
<script>
let count = $state(0)
</script>
{@debug count}
<button onclick={() => count++}> Increment </button>
<p>Count: {count}</p> Each click triggers the @debug tag, logging the new value of count to the console. The console output shows:
{count: 1}
{count: 2}
{count: 3} This simple pattern is perfect for tracking when a specific value updates during user interactions.
Multiple Value Tracking
Track multiple related values to understand how they change together:
<script>
let firstName = $state('')
let lastName = $state('')
let fullName = $derived(`${firstName} ${lastName}`.trim())
</script>
{@debug firstName, lastName, fullName}
<div class="form">
<input bind:value={firstName} placeholder="First Name" />
<input bind:value={lastName} placeholder="Last Name" />
<p>Full Name: {fullName}</p>
</div> The @debug tag fires whenever any tracked value changes, showing all current values:
{firstName: "A", lastName: "", fullName: "A"}
{firstName: "Al", lastName: "", fullName: "Al"}
{firstName: "Alice", lastName: "", fullName: "Alice"}
{firstName: "Alice", lastName: "C", fullName: "Alice C"}
{firstName: "Alice", lastName: "Ch", fullName: "Alice Ch"}
{firstName: "Alice", lastName: "Chen", fullName: "Alice Chen"} This reveals the reactive cascade: how changing firstName or lastName triggers the fullName derivation.
Parameterless Debug
Use @debug without parameters to create a reactive breakpoint:
<script>
let items = $state([])
function addItem() {
items = [...items, { id: Date.now(), value: Math.random() }]
}
</script>
{#if items.length > 5}
{@debug}
<p class="warning">Many items detected!</p>
{/if}
<button onclick={addItem}>Add Item</button>
<p>{items.length} items</p> When the condition becomes true, the parameterless @debug triggers a debugger breakpoint (if developer tools are open), pausing execution for inspection. This is particularly useful for catching state at critical moments.
Advanced Patterns
Debugging Derived State Chains
Complex derivations often involve multiple steps. Track the entire chain to understand propagation:
<script>
let rawData = $state([
{ value: 100, enabled: true },
{ value: 200, enabled: false },
{ value: 300, enabled: true }
])
let enabledItems = $derived(rawData.filter((item) => item.enabled))
let values = $derived(enabledItems.map((item) => item.value))
let sum = $derived(values.reduce((acc, val) => acc + val, 0))
let average = $derived(enabledItems.length ? sum / enabledItems.length : 0)
</script>
{@debug enabledItems, values, sum, average}
<div class="data-panel">
{#each rawData as item, i}
<label>
<input type="checkbox" bind:checked={item.enabled} />
Value: {item.value}
</label>
{/each}
<div class="results">
<p>Sum: {sum}</p>
<p>Average: {average.toFixed(2)}</p>
</div>
</div> Toggling checkboxes triggers the debug output, showing how the entire derivation chain updates:
{
enabledItems: [{value: 100, enabled: true}],
values: [100],
sum: 100,
average: 100
}
{
enabledItems: [{value: 100, enabled: true}, {value: 300, enabled: true}],
values: [100, 300],
sum: 400,
average: 200
} This makes it clear how enabling an item cascades through enabledItems → values → sum → average.
Conditional Debugging
Place @debug tags inside conditional blocks to debug specific scenarios:
<script>
let user = $state(null)
let permissions = $state([])
let isAdmin = $derived(permissions.includes('admin'))
let canEdit = $derived(permissions.includes('edit') || isAdmin)
</script>
{#if isAdmin}
{@debug user, permissions, isAdmin, canEdit}
<div class="admin-panel">
<h2>Admin Controls</h2>
<p>User: {user?.name}</p>
</div>
{:else if canEdit}
{@debug user, permissions, canEdit}
<div class="editor-panel">
<h2>Editor Controls</h2>
<p>User: {user?.name}</p>
</div>
{/if} Different @debug tags activate based on the permission level, logging only when the corresponding branch renders. This targeted approach reduces noise in the console, showing only relevant state for each scenario.
Loop-Scoped Debugging
Debug state within each iteration of a loop:
<script>
let tasks = $state([
{ id: 1, title: 'Review PR', completed: false, priority: 'high' },
{ id: 2, title: 'Update docs', completed: true, priority: 'medium' },
{ id: 3, title: 'Fix bug', completed: false, priority: 'critical' }
])
</script>
{#each tasks as task (task.id)}
{@const isUrgent = !task.completed && task.priority === 'critical'}
{#if isUrgent}
{@debug task, isUrgent}
{/if}
<div class="task" class:urgent={isUrgent}>
<label>
<input type="checkbox" bind:checked={task.completed} />
{task.title}
</label>
<span class="priority">{task.priority}</span>
</div>
{/each} The debug output fires only for urgent tasks, and updates when task completion status changes. This pinpoints issues in specific loop iterations without cluttering the console with every item.
Debugging Props and Bindings
Track incoming props and two-way bindings to understand parent-child communication:
<!-- Child.svelte -->
<script>
let { value = $bindable(), min = 0, max = 100 } = $props()
let clampedValue = $derived(Math.max(min, Math.min(max, value)))
let isAtLimit = $derived(value !== clampedValue)
</script>
{@debug value, clampedValue, min, max, isAtLimit}
<div class="slider-widget">
<input type="range" bind:value {min} {max} />
<span class="value">{clampedValue}</span>
{#if isAtLimit}
<span class="warning">At limit!</span>
{/if}
</div> <!-- Parent.svelte -->
<script>
import Child from './Child.svelte'
let sliderValue = $state(50)
</script>
{@debug sliderValue}
<Child bind:value={sliderValue} min={0} max={100} /> Now you can see the bidirectional flow: how parent state changes propagate to the child, and how child input updates flow back to the parent. The debug output from both components reveals the complete communication path.
Real-World Examples
Form Validation State
Track complex validation states across multiple fields:
<script>
let email = $state('')
let password = $state('')
let confirmPassword = $state('')
let agreeToTerms = $state(false)
let emailValid = $derived(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
let passwordValid = $derived(password.length >= 8)
let passwordsMatch = $derived(password === confirmPassword && password.length > 0)
let formValid = $derived(emailValid && passwordValid && passwordsMatch && agreeToTerms)
</script>
{@debug emailValid, passwordValid, passwordsMatch, agreeToTerms, formValid}
<form>
<label>
Email:
<input type="email" bind:value={email} />
{#if email && !emailValid}
<span class="error">Invalid email format</span>
{/if}
</label>
<label>
Password:
<input type="password" bind:value={password} />
{#if password && !passwordValid}
<span class="error">Password must be at least 8 characters</span>
{/if}
</label>
<label>
Confirm Password:
<input type="password" bind:value={confirmPassword} />
{#if confirmPassword && !passwordsMatch}
<span class="error">Passwords don't match</span>
{/if}
</label>
<label>
<input type="checkbox" bind:checked={agreeToTerms} />
I agree to the terms and conditions
</label>
<button type="submit" disabled={!formValid}> Sign Up </button>
</form> As users fill the form, the debug output shows exactly which validation conditions are passing or failing at each step, making it easy to identify why the submit button remains disabled.
Shopping Cart Calculations
Debug multi-step price calculations with discounts and taxes:
<script>
let cartItems = $state([
{ id: 1, name: 'Widget', price: 29.99, quantity: 2 },
{ id: 2, name: 'Gadget', price: 49.99, quantity: 1 },
{ id: 3, name: 'Gizmo', price: 19.99, quantity: 3 }
])
let couponCode = $state('')
let shippingMethod = $state('standard')
let subtotal = $derived(cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0))
let couponDiscount = $derived.by(() => {
if (couponCode === 'SAVE10') return subtotal * 0.1
if (couponCode === 'SAVE20') return subtotal * 0.2
return 0
})
let shippingCost = $derived.by(() => {
if (shippingMethod === 'express') return 15.99
if (shippingMethod === 'overnight') return 29.99
return 5.99
})
let taxableAmount = $derived(subtotal - couponDiscount + shippingCost)
let tax = $derived(taxableAmount * 0.08)
let total = $derived(taxableAmount + tax)
</script>
{@debug subtotal, couponDiscount, shippingCost, taxableAmount, tax, total}
<div class="cart">
<h2>Shopping Cart</h2>
{#each cartItems as item (item.id)}
<div class="cart-item">
<span>{item.name}</span>
<input type="number" bind:value={item.quantity} min="1" />
<span>${(item.price * item.quantity).toFixed(2)}</span>
</div>
{/each}
<div class="cart-controls">
<label>
Coupon Code:
<input bind:value={couponCode} placeholder="Enter code" />
</label>
<label>
Shipping:
<select bind:value={shippingMethod}>
<option value="standard">Standard ($5.99)</option>
<option value="express">Express ($15.99)</option>
<option value="overnight">Overnight ($29.99)</option>
</select>
</label>
</div>
<div class="cart-summary">
<div class="line-item">
<span>Subtotal:</span>
<span>${subtotal.toFixed(2)}</span>
</div>
{#if couponDiscount > 0}
<div class="line-item discount">
<span>Coupon Discount:</span>
<span>-${couponDiscount.toFixed(2)}</span>
</div>
{/if}
<div class="line-item">
<span>Shipping:</span>
<span>${shippingCost.toFixed(2)}</span>
</div>
<div class="line-item">
<span>Tax (8%):</span>
<span>${tax.toFixed(2)}</span>
</div>
<div class="line-item total">
<span>Total:</span>
<span>${total.toFixed(2)}</span>
</div>
</div>
</div> Every interaction—changing quantities, entering a coupon code, selecting shipping—triggers the debug output, revealing the complete calculation flow. This makes it trivial to verify that discounts apply before tax, that shipping is included in the taxable amount, and that all formulas work correctly.
Real-Time Filter Pipeline
Debug complex filtering and sorting operations:
<script>
let products = $state([
{ id: 1, name: 'Laptop', category: 'electronics', price: 999, inStock: true, rating: 4.5 },
{ id: 2, name: 'Desk', category: 'furniture', price: 299, inStock: true, rating: 4.2 },
{ id: 3, name: 'Chair', category: 'furniture', price: 199, inStock: false, rating: 4.7 },
{ id: 4, name: 'Mouse', category: 'electronics', price: 29, inStock: true, rating: 4.3 },
{ id: 5, name: 'Monitor', category: 'electronics', price: 399, inStock: true, rating: 4.6 }
]);
let categoryFilter = $state('all');
let minPrice = $state(0);
let maxPrice = $state(1000);
let showOutOfStock = $state(true);
let sortBy = $state('name');
let categoryFiltered = $derived(
categoryFilter === 'all'
? products
: products.filter((p) => p.category === categoryFilter)
);
let priceFiltered = $derived(
categoryFiltered.filter((p) => p.price >= minPrice && p.price <= maxPrice)
);
let stockFiltered = $derived(
showOutOfStock
? priceFiltered
: priceFiltered.filter((p) => p.inStock)
);
let sorted = $derived.by(() => {
const items = [...stockFiltered];
if (sortBy === 'name') return items.sort((a, b) => a.name.localeCompare(b.name));
if (sortBy === 'price') return items.sort((a, b) => a.price - b.price);
if (sortBy === 'rating') return items.sort((a, b) => b.rating - a.rating);
return items;
});
let categoryCount = $derived(categoryFiltered.length);
let priceCount = $derived(priceFiltered.length);
let stockCount = $derived(stockFiltered.length);
let sortedCount = $derived(sorted.length);
</script>
{@debug categoryCount, priceCount, stockCount, sortedCount}
<div class="product-browser">
<div class="filters">
<label>
Category:
<select bind:value={categoryFilter}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
<option value="furniture">Furniture</option>
</select>
</label>
<label>
Price Range:
<input type="number" bind:value={minPrice} min="0" />
to
<input type="number" bind:value={maxPrice} min="0" />
</label>
<label>
<input type="checkbox" bind:checked={showOutOfStock} />
Show out of stock
</label>
<label>
Sort by:
<select bind:value={sortBy}>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="rating">Rating</option>
</select>
</label>
</div>
<div class="product-grid">
{#each sorted as product (product.id)}
<div class="product-card" class:out-of-stock={!product.inStock}>
<h3>{product.name}</h3>
<p class="category">{product.category}</p>
<p class="price">${product.price}</p>
<p class="rating">★ {product.rating}</p>
{#if !product.inStock}
<p class="stock-status">Out of Stock</p>
{/if}
</div>
{/each}
</div>
<p class="result-count">
Showing {sorted.length} of {products.length} products
</p>
</div> The debug output shows how many items pass through each filter stage:
{categoryCount: 5, priceCount: 5, stockCount: 5, sortedCount: 5}
{categoryCount: 3, priceCount: 3, stockCount: 3, sortedCount: 3}
{categoryCount: 3, priceCount: 2, stockCount: 2, sortedCount: 2} This reveals exactly where items get filtered out, making it easy to verify filter logic and diagnose why certain products don’t appear.
Best Practices
Strategic Placement
Place @debug tags at decision points, not everywhere:
AVOID: Too Much Logging:
<script>
let a = $state(1)
let b = $state(2)
let c = $derived(a + b)
let d = $derived(c * 2)
let e = $derived(d - 1)
</script>
{@debug a}
{@debug b}
{@debug c}
{@debug d}
{@debug e} PREFERRED: Strategic Logging:
<script>
let a = $state(1)
let b = $state(2)
let c = $derived(a + b)
let d = $derived(c * 2)
let e = $derived(d - 1)
</script>
{@debug a, b, e} Track inputs and final outputs, not intermediate steps, unless you specifically need to debug the derivation chain.
Descriptive Context
When debugging complex scenarios, add comments to identify what you’re tracking:
<!-- Debugging user authentication flow -->
{@debug user, isAuthenticated, sessionToken}
<!-- Debugging cart total calculation -->
{@debug subtotal, discount, shippingCost, total} This helps when revisiting code later or when other developers need to understand your debugging approach.
Temporary vs. Permanent
Temporary debugging — Quick investigation:
{@debug someValue} Remove after solving the issue.
Permanent debugging — Development mode only:
{#if import.meta.env.DEV}
{@debug criticalState, userActions, systemHealth}
{/if} Keep debug tags in development builds but exclude them from production.
Combine with Conditional Rendering
Debug only when problems occur:
<script>
let apiResponse = $state(null)
let isError = $derived(apiResponse?.status === 'error')
</script>
{#if isError}
{@debug apiResponse}
<div class="error-panel">
<h3>Something went wrong</h3>
<p>{apiResponse.message}</p>
</div>
{/if} This ensures debug output only fires when the error condition is true, keeping your console clean during normal operation.
Common Pitfalls
Over-Logging
Tracking too many values creates noise:
AVOID: Excessive Tracking
{@debug var1, var2, var3, var4, var5, var6, var7, var8, var9, var10} PREFERRED: Focused Tracking
{@debug suspectedProblemValue, relatedDependency} Focus on the specific values involved in the issue you’re investigating.
Forgetting to Remove
Leaving @debug tags in production code:
AVOID: Left in Production
<script>
let userData = $state(null)
</script>
{@debug userData} PREFERRED: Environment-Aware
<script>
let userData = $state(null)
</script>
{#if import.meta.env.DEV}
{@debug userData}
{/if} Or remove entirely before committing to production branches.
Misunderstanding Reactive Scope
@debug only fires when tracked values change. If you’re not seeing output, the values might not be reactive:
AVOID: Non-Reactive Value
<script>
let config = { theme: 'dark', layout: 'grid' }
</script>
{@debug config} config is a regular variable. Changes to its properties won’t trigger the debug tag.
PREFERRED: Reactive Value
<script>
let config = $state({ theme: 'dark', layout: 'grid' })
</script>
{@debug config} Now config is reactive, and mutations will trigger the debug output.
Debugging Asynchronous State
@debug shows current values but doesn’t help with async timing:
Limited Visibility:
<script>
let data = $state(null)
let loading = $state(false)
async function fetchData() {
loading = true
data = await fetch('/api/data').then((r) => r.json())
loading = false
}
</script>
{@debug data, loading} You’ll see state changes but not the promise resolution timing. For async debugging, combine @debug with console.log in the async function:
<script>
let data = $state(null)
let loading = $state(false)
async function fetchData() {
loading = true
console.log('Fetch started')
data = await fetch('/api/data').then((r) => r.json())
console.log('Fetch completed', data)
loading = false
}
</script>
{@debug data, loading} This provides both reactive state tracking and async timing insights.
Integration with Developer Tools
Console Output Format
When @debug fires, it logs to the console with the tracked variable names as keys:
{@debug firstName, lastName, age} Console output:
{firstName: "Alice", lastName: "Chen", age: 29} This structured output integrates seamlessly with browser developer tools, allowing you to expand objects, arrays, and nested structures.
Breakpoint Behavior
Parameterless @debug acts as a conditional breakpoint:
{#if errorCondition}
{@debug}
{/if} When the condition is true and developer tools are open, execution pauses, allowing you to inspect the call stack and variable state. If developer tools are closed, the @debug tag has no effect.
Performance Considerations
@debug tags have minimal performance impact during development but should not be deployed to production. Use build-time environment checks to exclude them:
{#if import.meta.env.DEV}
{@debug importantState}
{/if} Most bundlers (Vite, Webpack, Rollup) will eliminate this block entirely in production builds through dead code elimination.
Comparison with Traditional Debugging
@debug vs. console.log
console.log approach:
<script>
let count = $state(0)
$effect(() => {
console.log('Count changed:', count)
})
</script> Requires manual $effect setup and cleanup. The effect runs on every reactive dependency change, not just count.
@debug approach:
<script>
let count = $state(0)
</script>
{@debug count} Automatic, declarative, and fires only when count changes. No need for manual effects or cleanup.
@debug vs. Breakpoints
Traditional breakpoints:
- Set in developer tools
- Static locations in code
- Break on every execution of that line
@debug breakpoints:
- Declared in template
- Reactive — only fire when conditions change
- Can be conditional based on template logic
Both approaches have value. Use @debug for reactive state tracking, and traditional breakpoints for imperative code flow.
Advanced Debugging Strategies
Debugging State Machines
Track state transitions in finite state machines:
<script>
let state = $state('idle')
let error = $state(null)
let data = $state(null)
async function load() {
state = 'loading'
error = null
try {
data = await fetch('/api/data').then((r) => r.json())
state = 'success'
} catch (e) {
error = e.message
state = 'error'
}
}
</script>
{@debug state, error, data}
<div class="state-machine">
{#if state === 'idle'}
<button onclick={load}>Load Data</button>
{:else if state === 'loading'}
<p>Loading...</p>
{:else if state === 'success'}
<div class="data">{JSON.stringify(data)}</div>
{:else if state === 'error'}
<div class="error">{error}</div>
{/if}
</div> The debug output shows each state transition, revealing the complete flow through the state machine.
Debugging Derived State Chains
Track derived values across a reactive chain:
<script>
let count = $state(0)
let doubled = $derived(count * 2)
let tripled = $derived(count * 3)
</script>
{@debug count, doubled, tripled}
<button onclick={() => count++}>Increment</button>
<p>Count: {count}, Doubled: {doubled}, Tripled: {tripled}</p> By tracking store values with the $ prefix, you see how store changes propagate through derived values.
Debugging Component Lifecycle
Track when components mount, update, and clean up:
<script>
let { data } = $props()
let mounted = $state(false)
let updateCount = $state(0)
$effect(() => {
mounted = true
return () => {
console.log('Component unmounting, updates:', updateCount)
}
})
$effect(() => {
data // Track data changes
updateCount++
})
</script>
{@debug mounted, updateCount, data}
<div class="component">
<p>Mounted: {mounted}</p>
<p>Update Count: {updateCount}</p>
<p>Data: {JSON.stringify(data)}</p>
</div> This combination of @debug, $effect, and console.log provides comprehensive lifecycle visibility.
Conclusion
The @debug tag transforms debugging from a tedious, manual process into a declarative, reactive experience that aligns perfectly with Svelte’s reactive model. By placing debug tags at strategic points in your templates, you gain real-time visibility into state changes, data flow, and component behavior without cluttering your script section with logging code.
The key to effective @debug usage lies in strategic placement: track inputs and outputs, not every intermediate value. Focus on decision points, derived computations, and reactive chains where understanding the current state is critical. Combine @debug with conditional rendering to activate logging only when problems occur, keeping your console clean during normal operation.
Remember that @debug is a development tool. Wrap debug tags in environment checks or remove them before production deployment. When used thoughtfully, @debug becomes an invaluable diagnostic tool that accelerates development, simplifies debugging, and deepens your understanding of reactive state management in Svelte 5.
Master @debug, and you’ll spend less time guessing about state changes and more time building features with confidence.
Key Takeaways
{@debug}provides declarative, reactive debugging that automatically logs values whenever dependencies change, triggering adebuggerbreakpoint when DevTools are open- Multiple variables can be tracked in one tag using
{@debug var1, var2, var3}syntax, logging all values together when any dependency changes - Strategic placement amplifies effectiveness - position debug tags at conditional boundaries, derived computation points, and before/after state mutations to track data flow
- Conditional debugging with
{#if}gates enables targeted logging only when specific conditions occur (e.g.,{#if error}{@debug error, context}{/if}) - Formatted logging output includes source location, component name, and structured value display with syntax highlighting in browser consoles
- Production safety requires environment checks - wrap debug tags with
{#if import.meta.env.DEV}or remove them entirely before deployment - Complementary to
$effectfor side-effect debugging - use@debugfor reactive logging and$effectfor programmatic inspection withconsole.trace()or custom logic - TypeScript integration provides type safety with proper inference for debugged variables and compile-time errors for undefined references
See Also
- Official Svelte 5 Documentation -
{@debug} - MDN Web Docs - debugger statement
- Chrome DevTools - JavaScript Debugging
$effect- Reactive side effects for programmatic debugging$inspect- Svelte 5’s enhanced debugging rune with formatted logging- Console API - Browser logging methods for debugging
- Vite Environment Variables - Managing development vs production modes