JavaScript Fortgeschritten

Async/Await in JavaScript meistern

Async/Await in JavaScript

async/await ist die moderne Art, asynchronen Code zu schreiben. Es macht Promise-basierten Code lesbar wie synchronen Code – keine Callback-Hölle, keine verschachtelten .then()-Ketten.

Warum async/await?

Vergleiche diese drei Ansätze:

Callbacks (Alt)

// Callback-Hölle 😱
getUser(userId, (user) => {
    getOrders(user.id, (orders) => {
        getOrderDetails(orders[0].id, (details) => {
            console.log(details);
        });
    });
});

Promises (Besser)

// Promise-Kette
getUser(userId)
    .then(user => getOrders(user.id))
    .then(orders => getOrderDetails(orders[0].id))
    .then(details => console.log(details))
    .catch(error => console.error(error));

Async/Await (Modern!)

// Klar und lesbar ✨
async function loadOrderDetails(userId) {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    const details = await getOrderDetails(orders[0].id);
    console.log(details);
}

Die Grundlagen

async Funktionen

Das async-Keyword macht eine Funktion asynchron:

// Normale Funktion
function normal() {
    return "Hallo";
}

// Async Funktion - gibt immer ein Promise zurück!
async function asyncVersion() {
    return "Hallo";
}

// Beide aufrufen
console.log(normal());        // "Hallo"
console.log(asyncVersion());  // Promise { "Hallo" }

// Async-Ergebnis nutzen
asyncVersion().then(result => console.log(result)); // "Hallo"

await verwenden

await wartet auf ein Promise und gibt dessen Wert zurück:

async function example() {
    // Promise erstellen
    const promise = new Promise(resolve => {
        setTimeout(() => resolve("Fertig!"), 1000);
    });

    console.log("Warte...");
    const result = await promise;  // Wartet 1 Sekunde
    console.log(result);  // "Fertig!"
}

example();
// Ausgabe:
// "Warte..."
// (1 Sekunde Pause)
// "Fertig!"

Wichtig: await funktioniert nur innerhalb von async-Funktionen!

// ❌ Fehler!
function notAsync() {
    const result = await somePromise; // SyntaxError!
}

// ✅ Korrekt
async function isAsync() {
    const result = await somePromise;
}

// ✅ Top-Level await (in Modulen)
const data = await fetch('/api/data');

Fehlerbehandlung mit try/catch

async function fetchUser(id) {
    try {
        const response = await fetch(`/api/users/${id}`);

        if (!response.ok) {
            throw new Error(`HTTP Error: ${response.status}`);
        }

        const user = await response.json();
        return user;

    } catch (error) {
        console.error("Fehler beim Laden:", error.message);
        return null;
    }
}

// Verwendung
const user = await fetchUser(123);
if (user) {
    console.log(`Willkommen, ${user.name}!`);
} else {
    console.log("User konnte nicht geladen werden");
}

Mehrere try/catch Blöcke

async function processOrder(orderId) {
    let order;
    let payment;

    // Order laden
    try {
        order = await fetchOrder(orderId);
    } catch (error) {
        console.error("Order nicht gefunden");
        return { success: false, step: "order" };
    }

    // Zahlung verarbeiten
    try {
        payment = await processPayment(order);
    } catch (error) {
        console.error("Zahlung fehlgeschlagen");
        return { success: false, step: "payment" };
    }

    return { success: true, order, payment };
}

Parallele Ausführung

Sequentiell (langsam)

async function loadSequential() {
    console.time("sequential");

    const users = await fetchUsers();      // 1 Sekunde
    const products = await fetchProducts(); // 1 Sekunde
    const orders = await fetchOrders();     // 1 Sekunde

    console.timeEnd("sequential");
    // sequential: ~3000ms 😴
}

Parallel mit Promise.all (schnell!)

async function loadParallel() {
    console.time("parallel");

    const [users, products, orders] = await Promise.all([
        fetchUsers(),
        fetchProducts(),
        fetchOrders()
    ]);

    console.timeEnd("parallel");
    // parallel: ~1000ms 🚀
}

Promise.all Fehlerbehandlung

async function loadAllOrNothing() {
    try {
        // Wenn EINER fehlschlägt, schlagen ALLE fehl
        const [users, products] = await Promise.all([
            fetchUsers(),
            fetchProducts()
        ]);
        return { users, products };
    } catch (error) {
        console.error("Mindestens ein Request fehlgeschlagen");
        return null;
    }
}

Promise.allSettled (Alle Ergebnisse)

async function loadAllResults() {
    const results = await Promise.allSettled([
        fetchUsers(),
        fetchProducts(),
        fetchOrders()
    ]);

    results.forEach((result, index) => {
        if (result.status === "fulfilled") {
            console.log(`Request ${index}: Erfolg`, result.value);
        } else {
            console.log(`Request ${index}: Fehler`, result.reason);
        }
    });

    // Nur erfolgreiche extrahieren
    const successfulData = results
        .filter(r => r.status === "fulfilled")
        .map(r => r.value);
}

Promise.race (Der Schnellste gewinnt)

async function fetchWithTimeout(url, timeout = 5000) {
    const fetchPromise = fetch(url);

    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error("Timeout!")), timeout);
    });

    // Wer zuerst fertig ist
    return Promise.race([fetchPromise, timeoutPromise]);
}

try {
    const response = await fetchWithTimeout("/api/data", 3000);
    const data = await response.json();
} catch (error) {
    console.log(error.message); // "Timeout!" wenn zu langsam
}

Fetch API mit async/await

GET Request

async function getUsers() {
    const response = await fetch("https://api.example.com/users");
    const users = await response.json();
    return users;
}

POST Request

async function createUser(userData) {
    const response = await fetch("https://api.example.com/users", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify(userData)
    });

    if (!response.ok) {
        throw new Error(`Error: ${response.status}`);
    }

    return response.json();
}

// Verwenden
const newUser = await createUser({
    name: "Max",
    email: "max@example.com"
});

Kompletter API-Service

const API_BASE = "https://api.example.com";

const api = {
    async get(endpoint) {
        const response = await fetch(`${API_BASE}${endpoint}`);
        if (!response.ok) throw new Error(`GET failed: ${response.status}`);
        return response.json();
    },

    async post(endpoint, data) {
        const response = await fetch(`${API_BASE}${endpoint}`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(data)
        });
        if (!response.ok) throw new Error(`POST failed: ${response.status}`);
        return response.json();
    },

    async put(endpoint, data) {
        const response = await fetch(`${API_BASE}${endpoint}`, {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(data)
        });
        if (!response.ok) throw new Error(`PUT failed: ${response.status}`);
        return response.json();
    },

    async delete(endpoint) {
        const response = await fetch(`${API_BASE}${endpoint}`, {
            method: "DELETE"
        });
        if (!response.ok) throw new Error(`DELETE failed: ${response.status}`);
        return response.ok;
    }
};

// Verwendung
const users = await api.get("/users");
const newUser = await api.post("/users", { name: "Max" });
await api.put("/users/1", { name: "Updated" });
await api.delete("/users/1");

Async in Loops

for…of (sequentiell)

async function processSequentially(items) {
    for (const item of items) {
        await processItem(item);  // Wartet auf jeden
    }
}

// Beispiel: Dateien nacheinander hochladen
async function uploadFiles(files) {
    for (const file of files) {
        console.log(`Uploading ${file.name}...`);
        await uploadFile(file);
        console.log(`${file.name} done!`);
    }
}

Promise.all mit map (parallel)

async function processParallel(items) {
    await Promise.all(
        items.map(item => processItem(item))
    );
}

// Beispiel: Alle User parallel laden
async function loadAllUsers(userIds) {
    const users = await Promise.all(
        userIds.map(id => fetchUser(id))
    );
    return users;
}

forEach funktioniert NICHT wie erwartet!

// ❌ FALSCH - forEach wartet nicht!
async function wrong(items) {
    items.forEach(async (item) => {
        await processItem(item);
    });
    console.log("Fertig"); // Wird sofort ausgegeben!
}

// ✅ RICHTIG - for...of verwenden
async function correct(items) {
    for (const item of items) {
        await processItem(item);
    }
    console.log("Fertig"); // Wird nach allen Items ausgegeben
}

Praktisches Beispiel: Wetter-App

const WEATHER_API = "https://api.weatherapi.com/v1";
const API_KEY = "your-api-key";

async function getWeather(city) {
    try {
        const response = await fetch(
            `${WEATHER_API}/current.json?key=${API_KEY}&q=${city}`
        );

        if (!response.ok) {
            if (response.status === 400) {
                throw new Error("Stadt nicht gefunden");
            }
            throw new Error("Wetterdaten konnten nicht geladen werden");
        }

        const data = await response.json();

        return {
            city: data.location.name,
            country: data.location.country,
            temp: data.current.temp_c,
            condition: data.current.condition.text,
            icon: data.current.condition.icon,
            humidity: data.current.humidity,
            wind: data.current.wind_kph
        };

    } catch (error) {
        console.error("Fehler:", error.message);
        return null;
    }
}

async function displayWeather(city) {
    const weatherDiv = document.querySelector("#weather");
    weatherDiv.textContent = "Laden...";

    const weather = await getWeather(city);

    if (weather) {
        weatherDiv.textContent = `
            ${weather.city}, ${weather.country}
            ${weather.temp}°C - ${weather.condition}
            Luftfeuchtigkeit: ${weather.humidity}%
            Wind: ${weather.wind} km/h
        `;
    } else {
        weatherDiv.textContent = "Wetter konnte nicht geladen werden";
    }
}

// Event Handler
document.querySelector("#search-btn").addEventListener("click", async () => {
    const city = document.querySelector("#city-input").value;
    await displayWeather(city);
});

Async Arrow Functions

// Als Variable
const fetchData = async () => {
    const response = await fetch("/api/data");
    return response.json();
};

// In Array-Methoden
const userIds = [1, 2, 3];
const users = await Promise.all(
    userIds.map(async (id) => {
        const response = await fetch(`/api/users/${id}`);
        return response.json();
    })
);

// Als Event Handler
button.addEventListener("click", async () => {
    await saveData();
    showNotification("Gespeichert!");
});

// Als IIFE (Immediately Invoked)
(async () => {
    const data = await loadInitialData();
    initApp(data);
})();

Async Class Methods

class UserService {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
    }

    async getUser(id) {
        const response = await fetch(`${this.baseUrl}/users/${id}`);
        return response.json();
    }

    async createUser(data) {
        const response = await fetch(`${this.baseUrl}/users`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(data)
        });
        return response.json();
    }

    async getUserWithPosts(id) {
        const [user, posts] = await Promise.all([
            this.getUser(id),
            this.getUserPosts(id)
        ]);
        return { ...user, posts };
    }

    async getUserPosts(userId) {
        const response = await fetch(`${this.baseUrl}/users/${userId}/posts`);
        return response.json();
    }
}

// Verwendung
const userService = new UserService("https://api.example.com");
const userWithPosts = await userService.getUserWithPosts(1);

Best Practices

1. Fehler immer behandeln

// Schlecht
async function bad() {
    const data = await fetch("/api"); // Unhandled rejection!
}

// Gut
async function good() {
    try {
        const data = await fetch("/api");
    } catch (error) {
        handleError(error);
    }
}

2. Parallel wenn möglich

// Unnötig langsam
const a = await fetchA();
const b = await fetchB();

// Schneller
const [a, b] = await Promise.all([fetchA(), fetchB()]);

3. Keine await in Loops wenn parallel möglich

// Langsam
for (const id of ids) {
    const user = await fetchUser(id);
}

// Schnell
const users = await Promise.all(ids.map(id => fetchUser(id)));

4. Timeout implementieren

async function fetchWithTimeout(url, timeout = 5000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
        const response = await fetch(url, {
            signal: controller.signal
        });
        return response;
    } finally {
        clearTimeout(timeoutId);
    }
}

Zusammenfassung

KonzeptBeschreibung
async functionMacht Funktion asynchron
awaitWartet auf Promise
try/catchFehlerbehandlung
Promise.all()Parallel ausführen
Promise.allSettled()Alle Ergebnisse (auch Fehler)
Promise.race()Der Schnellste gewinnt

Übung: Baue einen API-Client für eine öffentliche API (z.B. Pokemon API, GitHub API) mit async/await und Promise.all für parallele Requests!