lv.
a maple leave logo

Async Svelte: The await Keyword Just Got Merged


A Game-Changing Feature Just Landed

Svelte 5 just got a major upgrade with the ability to use the await keyword directly inside $derived runes! This feature fundamentally changes how we handle asynchronous operations in reactive computations, making async state management more elegant and powerful.

What This Means for Developers

Previously, handling async operations in derived state required complex workarounds or separate effect handlers. Now, you can write intuitive, readable code that treats async operations as first-class citizens in your reactive computations.

Basic Usage: Await in $derived

Here’s how you can now use await directly in $derived runes:

<script>
  let userId = $state(1);
  
  // This now works! 🎉
  let user = $derived(async () => {
    const response = await fetch(`/api/users/${userId}`);
    return await response.json();
  });
</script>

{#await user}
  <p>Loading user...</p>
{:then userData}
  <h1>Welcome, {userData.name}!</h1>
  <p>Email: {userData.email}</p>
{:catch error}
  <p>Error loading user: {error.message}</p>
{/await}

Advanced Example: Dependent Async Operations

The real power shows when chaining multiple async operations:

<script>
  let searchTerm = $state('');
  let selectedCategory = $state('all');
  
  // Search results that depend on both search term and category
  let searchResults = $derived(async () => {
    if (!searchTerm.trim()) return [];
    
    const response = await fetch(`/api/search?q=${searchTerm}&category=${selectedCategory}`);
    return await response.json();
  });
  
  // Get detailed info for the first result
  let firstResultDetails = $derived(async () => {
    const results = await searchResults;
    if (results.length === 0) return null;
    
    const detailResponse = await fetch(`/api/items/${results[0].id}`);
    return await detailResponse.json();
  });
</script>

<input bind:value={searchTerm} placeholder="Search..." />
<select bind:value={selectedCategory}>
  <option value="all">All Categories</option>
  <option value="products">Products</option>
  <option value="articles">Articles</option>
</select>

{#await searchResults}
  <p>Searching...</p>
{:then results}
  <div class="results">
    {#each results as result}
      <div class="result-item">
        <h3>{result.title}</h3>
        <p>{result.description}</p>
      </div>
    {/each}
  </div>
  
  {#if results.length > 0}
    {#await firstResultDetails}
      <p>Loading details...</p>
    {:then details}
      {#if details}
        <div class="featured-details">
          <h2>Featured: {details.title}</h2>
          <p>{details.fullDescription}</p>
        </div>
      {/if}
    {/await}
  {/if}
{:catch error}
  <p>Search failed: {error.message}</p>
{/await}

Error Handling and Loading States

One of the benefits of this approach is that you can handle errors and loading states naturally using Svelte’s existing {#await} blocks:

<script>
  let postId = $state(1);
  
  let post = $derived(async () => {
    const response = await fetch(`/api/posts/${postId}`);
    if (!response.ok) {
      throw new Error(`Failed to fetch post: ${response.statusText}`);
    }
    return await response.json();
  });
  
  let comments = $derived(async () => {
    const postData = await post;
    const response = await fetch(`/api/posts/${postData.id}/comments`);
    return await response.json();
  });
</script>

<div class="post-container">
  {#await post}
    <div class="loading">Loading post...</div>
  {:then postData}
    <article>
      <h1>{postData.title}</h1>
      <p>{postData.content}</p>
      <div class="meta">
        <span>By {postData.author}</span>
        <span>{postData.publishedAt}</span>
      </div>
    </article>
    
    <section class="comments">
      <h2>Comments</h2>
      {#await comments}
        <p>Loading comments...</p>
      {:then commentList}
        {#each commentList as comment}
          <div class="comment">
            <strong>{comment.author}</strong>
            <p>{comment.content}</p>
          </div>
        {/each}
      {:catch error}
        <p>Failed to load comments: {error.message}</p>
      {/await}
    </section>
  {:catch error}
    <div class="error">
      <h2>Error</h2>
      <p>{error.message}</p>
    </div>
  {/await}
</div>

Performance Benefits

This feature brings several performance advantages:

1. Automatic Caching

Derived values with async operations are automatically cached, so identical requests won’t trigger unnecessary network calls:

<script>
  let userId = $state(1);
  
  // This will only fetch once per unique userId
  let user = $derived(async () => {
    console.log('Fetching user:', userId); // Only logs on actual fetch
    const response = await fetch(`/api/users/${userId}`);
    return await response.json();
  });
</script>

2. Reactive Dependencies

The async derived values automatically track their dependencies and only re-run when necessary:

<script>
  let filters = $state({ category: 'all', sort: 'name' });
  
  // Only refetches when filters actually change
  let filteredData = $derived(async () => {
    const params = new URLSearchParams(filters);
    const response = await fetch(`/api/data?${params}`);
    return await response.json();
  });
</script>

Comparison with Previous Approaches

Before: Complex State Management

<script>
  let userId = $state(1);
  let user = $state(null);
  let loading = $state(false);
  let error = $state(null);
  
  $effect(async () => {
    loading = true;
    error = null;
    try {
      const response = await fetch(`/api/users/${userId}`);
      user = await response.json();
    } catch (e) {
      error = e;
    } finally {
      loading = false;
    }
  });
</script>

After: Clean and Intuitive

<script>
  let userId = $state(1);
  
  let user = $derived(async () => {
    const response = await fetch(`/api/users/${userId}`);
    return await response.json();
  });
</script>

Best Practices

1. Handle Errors Gracefully

Always use {#await} blocks with {:catch} to handle potential errors:

{#await asyncDerived}
  <p>Loading...</p>
{:then data}
  <p>Data: {data}</p>
{:catch error}
  <p>Error: {error.message}</p>
{/await}

2. Consider Loading States

Provide meaningful loading states for better UX:

{#await userData}
  <div class="skeleton">
    <div class="skeleton-line"></div>
    <div class="skeleton-line"></div>
  </div>
{:then user}
  <UserProfile {user} />
{/await}

3. Avoid Infinite Loops

Be careful not to create circular dependencies:

<script>
  // ❌ Don't do this - creates circular dependency
  let a = $derived(async () => {
    const b = await bValue;
    return b + 1;
  });
  
  let bValue = $derived(async () => {
    const a = await aValue;
    return a + 1;
  });
</script>

Integration with Existing Await Blocks

This feature works seamlessly with Svelte’s existing {#await} blocks from the Svelte await documentation. You can still use the traditional await block syntax for promises that aren’t derived state:

<script>
  let promise = $state(fetch('/api/data'));
  
  // Traditional await block - still works great
  function handleClick() {
    promise = fetch('/api/data');
  }
</script>

<button onclick={handleClick}>Refresh</button>

{#await promise}
  <p>Loading...</p>
{:then response}
  <p>Status: {response.status}</p>
{:catch error}
  <p>Error: {error.message}</p>
{/await}

Real-World Example: Dashboard

Here’s a complete example of a dashboard that uses async derived values:

<script>
  let selectedMetric = $state('sales');
  let timeRange = $state('7d');
  
  let metrics = $derived(async () => {
    const response = await fetch(`/api/metrics/${selectedMetric}?range=${timeRange}`);
    return await response.json();
  });
  
  let insights = $derived(async () => {
    const metricData = await metrics;
    const response = await fetch('/api/insights', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ data: metricData })
    });
    return await response.json();
  });
</script>

<div class="dashboard">
  <div class="controls">
    <select bind:value={selectedMetric}>
      <option value="sales">Sales</option>
      <option value="users">Users</option>
      <option value="revenue">Revenue</option>
    </select>
    
    <select bind:value={timeRange}>
      <option value="1d">1 Day</option>
      <option value="7d">7 Days</option>
      <option value="30d">30 Days</option>
    </select>
  </div>
  
  <div class="content">
    {#await metrics}
      <div class="loading">Loading metrics...</div>
    {:then metricData}
      <div class="metric-chart">
        <h2>{selectedMetric} - {timeRange}</h2>
        <p>Total: {metricData.total}</p>
        <p>Growth: {metricData.growth}%</p>
      </div>
      
      {#await insights}
        <div class="loading">Generating insights...</div>
      {:then insightData}
        <div class="insights">
          <h3>AI Insights</h3>
          <ul>
            {#each insightData.suggestions as suggestion}
              <li>{suggestion}</li>
            {/each}
          </ul>
        </div>
      {/await}
    {:catch error}
      <div class="error">Failed to load dashboard: {error.message}</div>
    {/await}
  </div>
</div>

<style>
  .dashboard {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
  }
  
  .controls {
    display: flex;
    gap: 10px;
    margin-bottom: 20px;
  }
  
  .loading {
    text-align: center;
    padding: 40px;
    color: #666;
  }
  
  .error {
    background: #fee;
    border: 1px solid #fcc;
    padding: 20px;
    border-radius: 4px;
    color: #c00;
  }
</style>

Conclusion

The addition of await support in $derived runes marks a significant evolution in Svelte’s reactive system. It eliminates the complexity of managing async state manually and provides a clean, intuitive API for handling asynchronous operations in reactive computations.

This feature makes Svelte 5 even more powerful for building modern web applications that rely heavily on asynchronous data fetching and processing. Combined with Svelte’s existing {#await} blocks, you now have a complete toolkit for handling async operations at every level of your application.

The reactive nature of these async derived values means they automatically update when their dependencies change, providing a seamless user experience while maintaining excellent performance characteristics.

Get ready to simplify your async code and build more responsive applications with this powerful new feature! 🚀