By day 9 of the Svelte journey, you've written reactive stores and component props. Now comes the layer that ties data to the DOM: template syntax essentials. Understanding the patterns, pitfalls and fixes around Svelte's logic blocks will separate rough-and-tumble prototypes from production-grade components.
Conditional rendering with {#if} {:else} {/if}
The {#if} block is your gateway to declarative conditionals. Unlike imperative frameworks where you might juggle className toggling, Svelte removes the entire subtree from the DOM when the condition is false. This means event listeners, state subscriptions, and child component initialization don't happen until needed.
Consider a real-world scenario: you're building a checkout form where a discount code input only appears when the user has eligibility. Here's the pattern:
<script>
let userTier = 'free';
let showDiscount = false;
function checkEligibility() {
showDiscount = userTier === 'premium' || userTier === 'enterprise';
}
</script>
{#if showDiscount}
<div class="discount-section">
<label for="code">Discount Code</label>
<input id="code" type="text" placeholder="Enter code" />
</div>
{:else}
<p>Upgrade to premium to unlock discount codes.</p>
{/if}The {:else} clause is optional but important for user experience. Without it, the UI simply vanishes, which can feel broken. With it, you provide context. You can also chain conditions with {:else if}:
{#if userTier === 'enterprise'}
<p>Enterprise support included.</p>
{:else if userTier === 'premium'}
<p>Premium support available.</p>
{:else}
<p>Free tier - limited support.</p>
{/if}Each branch is mutually exclusive, so Svelte tears down and rebuilds only the active branch when conditions change. This keeps your template readable and your performance predictable.
List rendering with {#each items as item (item.id)}
The {#each} block iterates over arrays and objects, and the key expression in parentheses is non-negotiable in production code. Without it, Svelte uses positional indices, which causes bugs when lists reorder or items are inserted in the middle.
Imagine you're rendering a table of active database connections in a monitoring dashboard. Connections disconnect and reconnect randomly. Without keying, the rows get confused:
<script>
let connections = [
{ id: '1', host: 'db-1.internal', status: 'active' },
{ id: '2', host: 'db-2.internal', status: 'idle' },
];
</script>
<!-- ❌ Without key - indices cause mismatch -->
{#each connections as conn}
<tr>
<td>{conn.host}</td>
<td>{conn.status}</td>
</tr>
{/each}
<!-- ✅ With key - identity preserved -->
{#each connections as conn (conn.id)}
<tr>
<td>{conn.host}</td>
<td>{conn.status}</td>
</tr>
{/each}The key must be unique within the list. Use database IDs, UUIDs, or stable identifiers. Avoid array indices or timestamps that might collide.
You can also destructure within the each block and access the index:
{#each connections as { id, host, status }, idx (id)}
<tr>
<td>{idx + 1}</td>
<td>{host}</td>
<td>{status}</td>
</tr>
{/each}If you need to handle empty lists, combine {#each} with {#if} to show a fallback message. Svelte doesn't have a built-in empty state block, so this is the established pattern:
{#if connections.length === 0}
<p>No active connections.</p>
{:else}
<table>
{#each connections as conn (conn.id)}
<tr>...</tr>
{/each}
</table>
{/if}Async handling with {#await promise}
The {#await} block unwraps promises and gives you pending, success, and error states without writing imperative .then() chains or managing loading flags manually. This is invaluable for data fetching scenarios in SvelteKit where you're loading user profiles, API responses, or file uploads.
Here's a real example: fetching a user's transaction history on page load:
<script>
let transactionPromise = fetch('/api/transactions').then(r => r.json());
</script>
{#await transactionPromise}
<p>Loading transactions...</p>
{:then transactions}
<ul>
{#each transactions as tx (tx.id)}
<li>{tx.date}: {tx.amount} ({tx.type})</li>
{/each}
</ul>
{:catch error}
<p style="color: red;">Error: {error.message}</p>
{/await}The pending block shows while the promise is in flight. The {:then} block receives the resolved value and can iterate, format, or display it. The {:catch} block handles rejections gracefully.
In SvelteKit with server-side rendering, you often work with page data passed from +page.server.js. You might still wrap secondary fetches in {#await} to avoid blocking the initial render:
<script>
export let data;
$: recommendations = fetch(`/api/users/${data.userId}/recommendations`)
.then(r => r.json());
</script>
<h1>{data.user.name}</h1>
{#await recommendations}
<p>Personalizing recommendations...</p>
{:then items}
<ul>
{#each items as item (item.id)}
<li>{item.title}</li>
{/each}
</ul>
{:catch}
<p>Could not load recommendations.</p>
{/await}You can omit the {:catch} block if you don't need error handling, but catching errors prevents unhandled promise rejections in the browser console.
HTML injection with {@html} only with trusted content
Svelte escapes text content by default, converting <, >, and & to HTML entities. This is safe. But when you need raw HTML—rich text from a WYSIWYG editor, markdown rendered to HTML, or SVG embedded in a response—you reach for the {@html} directive.
Never use {@html} with untrusted input. If a user can control the string, an attacker can inject script tags and steal sessions. This is cross-site scripting (XSS).
Here's the right and wrong way. Say you're displaying a blog post body stored in your database:
<script>
let post = {
title: 'How We Scale',
body: '<p>We use...</p>', // From your CMS, sanitized on ingest
};
</script>
<!-- ❌ Escaped, shows raw HTML as text -->
<div>{post.body}</div>
<!-- ✅ Renders HTML, but only if you own the content -->
<div>{@html post.body}</div>The key assumption is that {@html post.body} contains content you control or have already sanitized. In a CMS workflow, sanitize on write: strip script tags, iframes, event handlers. Use a library like DOMPurify if you're unsure:
import DOMPurify from 'isomorphic-dompurify';
export async function load({ params }) {
const post = await db.posts.findById(params.slug);
return {
title: post.title,
body: DOMPurify.sanitize(post.body), // Cleaned server-side
};
}Then {@html body} is safe because the server already vetted it. In SvelteKit, this pattern puts sanitization at the boundary where data enters, not at the template.
Common mistakes
- Forgetting the key in {#each}: Arrays reorder, rows get mixed up, form state goes to the wrong item. Always use (stable.id).
- Storing promises in reactive variables without {@await}: If you assign a promise to a reactive variable, you'll see "[object Promise]" in the DOM. Wrap it in {#await} or use a store with .then() to unwrap synchronously.
- Nesting {#if} inside {#each} without key: The combination works, but forgetting the key on the each block will still cause bugs even if the if block works fine.
- Using {@html} with user input: No sanitization = XSS vulnerability. If data comes from a form, API endpoint, or URL param, it's untrusted. Sanitize it first or escape it.
- Conditional blocks that change markup structure: If you use {#if} to toggle between two entirely different components or form layouts, Svelte re-initializes both on each toggle. This can lose form state or trigger unnecessary animations. Use {#if} for fine-grained visibility; consider a store or prop for larger structural changes.
Key takeaways
- Use {#if} {:else} {/if} for reactive conditionals; the DOM subtree is removed when false, avoiding stale listeners and state.
- Always provide a key expression (id, uuid) in {#each} blocks to preserve item identity across reorders and mutations.
- Unwrap promises with {#await} pending, {:then}, {:catch} to avoid manual loading state and handle errors cleanly in templates.
- {@html} is safe only with trusted content; sanitize untrusted input on the server before it reaches the template.
- Template syntax essentials in Svelte are declarative and intentional; patterns, pitfalls and fixes become second nature after a few production cycles.






Comments
Loading comments…