Zum Inhalt springen

Promises & async/await

JavaScript ist single-threaded — es gibt nur einen Thread, der Code ausführt. Trotzdem kann JavaScript auf Netzwerkantworten warten, Timer setzen oder Dateien lesen, ohne den Rest der Anwendung zu blockieren. Das funktioniert über asynchronen Code.

Stell dir vor, du lädst Daten von einer API. Wenn JavaScript synchron darauf warten würde, wäre die gesamte Seite eingefroren:

// Hypothetisch synchron — so funktioniert es NICHT
const daten = fetchSync("/api/studenten"); // Blockiert alles für 2 Sekunden
console.log(daten); // Erst danach geht es weiter

Deshalb läuft Netzwerk-Kommunikation asynchron: Der Code startet die Anfrage und macht sofort weiter. Das Ergebnis kommt später.

Die älteste Lösung sind Callbacks — Funktionen, die aufgerufen werden, wenn die Arbeit fertig ist:

setTimeout(() => {
console.log("2 Sekunden vergangen");
}, 2000);
console.log("Das kommt zuerst");

Ausgabe:

Das kommt zuerst
2 Sekunden vergangen

Das funktioniert, aber bei mehreren aufeinanderfolgenden asynchronen Operationen wird es schnell unübersichtlich:

// "Callback Hell"
getUser(userId, (user) => {
getCourses(user.id, (courses) => {
getGrades(courses[0].id, (grades) => {
console.log(grades);
});
});
});

Ein Promise ist ein Objekt, das einen zukünftigen Wert repräsentiert. Es kann in drei Zuständen sein:

ZustandBedeutung
pendingDie Arbeit läuft noch
fulfilledErfolgreich abgeschlossen — der Wert ist verfügbar
rejectedFehlgeschlagen — ein Fehler ist aufgetreten

Die meisten APIs geben dir fertige Promises zurück. fetch ist das bekannteste Beispiel:

const promise = fetch("/api/studenten");
console.log(promise); // Promise { <pending> }

Um an den Wert zu kommen, verwendest du .then():

fetch("/api/studenten")
.then(response => response.json())
.then(daten => {
console.log(daten); // Die geladenen Daten
});

Jedes .then() gibt selbst ein Promise zurück — deshalb kannst du sie verketten:

fetch("/api/studenten")
.then(response => response.json())
.then(daten => daten.filter(s => s.aktiv))
.then(aktive => {
console.log(aktive);
});

Wenn ein Promise fehlschlägt oder ein .then()-Callback einen Fehler wirft, springt die Kette zum nächsten .catch():

fetch("/api/studenten")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(daten => {
console.log(daten);
})
.catch(error => {
console.error("Fehler beim Laden:", error.message);
});

.catch() fängt Fehler aus allen vorherigen .then()-Schritten auf.

Code in .finally() läuft immer — egal ob Erfolg oder Fehler:

fetch("/api/studenten")
.then(response => response.json())
.then(daten => console.log(daten))
.catch(error => console.error(error))
.finally(() => {
console.log("Anfrage abgeschlossen");
});

async/await ist syntaktischer Zucker über Promises — derselbe Mechanismus, aber mit einer Syntax, die sich wie synchroner Code liest:

async function ladeStudenten() {
const response = await fetch("/api/studenten");
const daten = await response.json();
console.log(daten);
}

await pausiert die Funktion, bis das Promise erfüllt ist, und gibt den Wert zurück. Wichtig: await funktioniert nur innerhalb einer async-Funktion.

Statt .catch() verwendest du try/catch:

async function ladeStudenten() {
try {
const response = await fetch("/api/studenten");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const daten = await response.json();
console.log(daten);
} catch (error) {
console.error("Fehler:", error.message);
} finally {
console.log("Anfrage abgeschlossen");
}
}

Auch Arrow Functions können async sein:

const ladeStudenten = async () => {
const response = await fetch("/api/studenten");
return response.json();
};

Manchmal musst du selbst ein Promise erstellen — zum Beispiel um einen Timer in ein Promise zu verpacken:

function warte(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function demo() {
console.log("Start");
await warte(2000);
console.log("2 Sekunden später");
}

Der Promise-Konstruktor erhält eine Funktion mit zwei Parametern:

  • resolve(wert) — das Promise erfolgreich abschließen
  • reject(fehler) — das Promise mit einem Fehler abschließen
function ladeJSON(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(response => {
if (!response.ok) reject(new Error(`HTTP ${response.status}`));
return response.json();
})
.then(resolve)
.catch(reject);
});
}

Wenn du mehrere unabhängige Anfragen gleichzeitig starten willst:

Promise.all — wartet auf alle Promises. Schlägt fehl, wenn eines fehlschlägt:

async function ladeDashboard(userId) {
const [user, kurse, noten] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/users/${userId}/kurse`).then(r => r.json()),
fetch(`/api/users/${userId}/noten`).then(r => r.json()),
]);
console.log(user, kurse, noten);
}

Alle drei Anfragen laufen gleichzeitig — das ist schneller als nacheinander.

Promise.allSettled — wartet auf alle, auch wenn manche fehlschlagen:

const ergebnisse = await Promise.allSettled([
fetch("/api/studenten"),
fetch("/api/kurse"),
]);
ergebnisse.forEach(result => {
if (result.status === "fulfilled") {
console.log("Erfolg:", result.value);
} else {
console.log("Fehler:", result.reason);
}
});

In React lädst du Daten typischerweise in einem useEffect:

function StudentenListe() {
const [studenten, setStudenten] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function ladeDaten() {
try {
const response = await fetch("/api/studenten");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const daten = await response.json();
setStudenten(daten);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
ladeDaten();
}, []);
if (loading) return <p>Lädt...</p>;
if (error) return <p>Fehler: {error}</p>;
return (
<ul>
{studenten.map(s => (
<li key={s.id}>{s.name}</li>
))}
</ul>
);
}