Promises lassen sich in Svelte direkt im Template behandeln — kein useEffect-Konstrukt, keine separate State-Variable für Loading/Error. Der {#await}-Block deckt mit {:then} (Erfolg) und {:catch} (Fehler) alle drei Zustände ab. Dieser Artikel zeigt die Varianten, die Kombination mit fetch und das Verhältnis zu SvelteKit-load-Funktionen.
Grundsyntax
<script>
let promise = fetch('/api/users/1').then((r) => r.json());
</script>
{#await promise}
<p>Lädt …</p>
{:then user}
<p>Hallo, {user.name}</p>
{:catch error}
<p style="color: red">Fehler: {error.message}</p>
{/await}Drei Zweige:
{#await promise}– Loading-Zustand, solange das Promise pending ist.{:then result}– Erfolg, der Wert ist im Block verfügbar.{:catch error}– Fehler, der Error-Wert ist im Block verfügbar.
Kurzform ohne Loading
Wenn kein Loading-State angezeigt werden soll (z. B. weil Server-Side-Rendering die Daten schon liefert), reicht eine Kurzform:
{#await promise then user}
<p>Hallo, {user.name}</p>
{/await}Während des Pending-Zustands wird hier gar nichts gerendert.
Nur Loading + Erfolg ohne Catch
Wenn catch an anderer Stelle behandelt wird (z. B. Error Boundary), kann er weggelassen werden:
{#await promise}
<Spinner />
{:then user}
<UserCard {user} />
{/await}Bei einer Fehler im Promise wird die nächste umschließende <svelte:boundary>-Komponente ausgelöst.
Reaktivität bei Promise-Wechsel
Wenn die Promise-Variable wechselt (z. B. weil sich eine userId ändert), reagiert der Block automatisch — neuer Loading-Zustand, neue Auflösung:
<script>
let userId = $state(1);
let promise = $derived(
fetch(`/api/users/${userId}`).then((r) => r.json())
);
</script>
<select bind:value={userId}>
<option value={1}>User 1</option>
<option value={2}>User 2</option>
<option value={3}>User 3</option>
</select>
{#await promise}
<p>Lädt …</p>
{:then user}
<p>{user.name}</p>
{:catch error}
<p>Fehler: {error.message}</p>
{/await}Beim Auswählen einer anderen ID wird ein neues Promise erstellt — der Block springt automatisch zurück in den Loading-Zustand.
Verwendung mit async function im Script
Statt das Promise inline zu erzeugen, oft besser eine Funktion definieren:
<script>
let userId = $state(1);
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('Nicht gefunden');
return res.json();
}
let promise = $derived(loadUser(userId));
</script>
{#await promise}
<Spinner />
{:then user}
<UserCard {user} />
{:catch error}
<ErrorBanner {error} />
{/await}Vorteil: Der Code ist testbar und wiederverwendbar.
TypeScript: typed values
{:then result} erbt den Promise-Typ — IDE und Compiler kennen result.foo korrekt:
<script lang="ts">
type User = { id: number; name: string; email: string };
async function loadUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
let userId = $state(1);
let promise = $derived(loadUser(userId));
</script>
{#await promise}
<p>Lädt …</p>
{:then user}
<p>{user.email}</p> <!-- user: User -->
{:catch error}
<p>{(error as Error).message}</p>
{/await}{#await} vs. SvelteKit-load
In SvelteKit gibt es einen anderen Weg, Daten zu laden: die load-Funktion. Sie läuft auf dem Server (oder im Browser) bevor die Seite gerendert wird, und liefert Daten als data-Prop.
| Aspekt | {#await} | SvelteKit-load |
|---|---|---|
| Wann läuft? | Während des Renderns (Client) | Vor dem Rendering (SSR & CSR) |
| Loading-Anzeige | Eingebaut ({#await}) | Manuell oder via streamed-Pattern |
| SEO | Daten erst nach Hydration sichtbar | Daten Teil des SSR-HTML |
| TypeScript | Inferred aus Promise | Generiert via $types |
| Geeignet für | Lazy/optionale Daten, Drittsysteme | Page-Daten, Authentifizierung, SEO |
Faustregel: Page-Daten gehören in load, optionale oder lazy Daten in {#await}.
Mehr zur load-Funktion im Artikel SvelteKit Routing & Loading.
Praxis-Beispiel: User-Profil mit Retry
<script>
let { userId } = $props();
let attempt = $state(0);
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
let promise = $derived(loadUser(userId));
// Re-Trigger bei Klick auf Retry
$effect(() => { attempt; });
</script>
{#await promise}
<Spinner />
{:then user}
<article>
<h1>{user.name}</h1>
<p>{user.email}</p>
</article>
{:catch error}
<div class="error">
<p>Fehler: {error.message}</p>
<button onclick={() => attempt++}>Erneut versuchen</button>
</div>
{/await}Anmerkung: Für ein „echtes Retry” reicht das Erhöhen einer Zähler-Variable nicht aus, wenn loadUser rein vom userId abhängt. Hier: userId und attempt beide in loadUser einbeziehen oder mit dedizierter Wrapper-Logik arbeiten.
Häufige Stolperfallen
Promise im Render-Body neu erzeugt.
{#await fetch('/api/data').then((r) => r.json())}
<Spinner />
{/await}Bei jedem Re-Render wird ein neues Promise erzeugt — Block bleibt im Loading-State. Lieber das Promise im Script in einer Variable halten.
Race-Condition bei sich ändernder ID.
Wenn die User-ID schnell wechselt, kann eine alte Promise-Resolution eine spätere überschreiben. In komplexeren Fällen mit AbortController oder einer Bibliothek wie TanStack Query absichern.
error als unknown.
TypeScript typisiert catch-Werte als unknown (oder Error je nach Config). Ein expliziter Cast (error as Error).message oder eine instanceof-Prüfung sind oft nötig.
Errors verschluckt.
Ohne {:catch} und ohne umschließende Boundary werden Promise-Errors als unhandled-Rejection geloggt, aber im UI taucht nichts auf. In Production zu vermeiden.