19 Promises und Promise-Chaining

Promises stellen einen bedeutenden Fortschritt in der Handhabung asynchroner Operationen in JavaScript dar. Sie wurden mit ECMAScript 6 (ES2015) standardisiert und bieten eine elegantere Alternative zu verschachtelten Callbacks. In diesem Abschnitt werden wir das Konzept, die Funktionsweise und die praktischen Anwendungen von Promises detailliert betrachten.

19.1 Grundkonzept von Promises

Ein Promise ist ein Objekt, das einen eventuellen Abschluss (oder Fehlschlag) einer asynchronen Operation und deren resultierenden Wert repräsentiert. Es dient als Stellvertreter für einen Wert, der zum Zeitpunkt der Promise-Erstellung möglicherweise noch nicht bekannt ist.

Ein Promise kann sich in einem von drei Zuständen befinden:

19.2 Anatomie eines Promise

Die Erstellung eines Promise erfolgt über den Promise-Konstruktor, der eine Funktion (den “Executor”) mit zwei Parametern erwartet: resolve und reject:

const meinPromise = new Promise((resolve, reject) => {
  // Asynchrone Operation
  const erfolgreich = true;
  
  if (erfolgreich) {
    resolve('Operation erfolgreich abgeschlossen!');
  } else {
    reject(new Error('Operation fehlgeschlagen.'));
  }
});

19.3 Interaktion mit Promises: then(), catch() und finally()

Ein Promise bietet drei Hauptmethoden zur Interaktion:

meinPromise
  .then((ergebnis) => {
    console.log('Erfolg:', ergebnis);
    return 'Neuer Wert'; // Kann für Chaining verwendet werden
  })
  .catch((fehler) => {
    console.error('Fehler:', fehler);
    throw new Error('Weiterer Fehler'); // Oder einen neuen Fehler werfen
  })
  .finally(() => {
    console.log('Diese Ausführung erfolgt immer, unabhängig vom Ergebnis');
  });

19.4 Ein praktisches Beispiel: Datenabruf

Hier ein Beispiel, das die Verwendung von Promises für einen Datenabruf demonstriert:

function holeDatenVomServer(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    
    xhr.onload = function() {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error(`Anfrage fehlgeschlagen mit Status ${xhr.status}`));
      }
    };
    
    xhr.onerror = function() {
      reject(new Error('Netzwerkfehler aufgetreten'));
    };
    
    xhr.send();
  });
}

holeDatenVomServer('https://api.beispiel.de/benutzer/1')
  .then(daten => {
    console.log('Benutzerdaten erfolgreich geladen:', daten);
  })
  .catch(fehler => {
    console.error('Fehler beim Laden der Daten:', fehler);
  });

19.5 Promise-Chaining: Verkettung asynchroner Operationen

Einer der größten Vorteile von Promises ist die Möglichkeit der Verkettung (Chaining). Dies ermöglicht es, mehrere asynchrone Operationen in einer linearen, leicht lesbaren Weise zu verknüpfen:

function holeBenutzerId() {
  return new Promise((resolve) => {
    setTimeout(() => resolve(42), 1000);
  });
}

function holeBenutzerDetails(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        id: id,
        name: 'Max Mustermann',
        email: 'max@beispiel.de'
      });
    }, 1000);
  });
}

function holeBenutzerPosts(benutzer) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        benutzer: benutzer,
        posts: [
          { id: 1, titel: 'Erster Beitrag' },
          { id: 2, titel: 'Zweiter Beitrag' }
        ]
      });
    }, 1000);
  });
}

// Promise-Chaining
holeBenutzerId()
  .then(id => {
    console.log('Benutzer-ID erhalten:', id);
    return holeBenutzerDetails(id);
  })
  .then(benutzer => {
    console.log('Benutzerdetails erhalten:', benutzer);
    return holeBenutzerPosts(benutzer);
  })
  .then(daten => {
    console.log('Benutzer mit Posts:', daten);
  })
  .catch(fehler => {
    console.error('Ein Fehler ist aufgetreten:', fehler);
  });

Jeder .then()-Aufruf:

  1. Wartet auf die Auflösung des vorherigen Promise
  2. Verarbeitet das Ergebnis durch die angegebene Callback-Funktion
  3. Gibt ein neues Promise zurück, das mit dem Rückgabewert der Callback-Funktion aufgelöst wird

Dies vermeidet die “Callback-Hölle” und sorgt für einen linearen, leichter lesbaren Code.

19.6 Fehlerbehandlung in Promise-Ketten

Ein weiterer großer Vorteil von Promises ist die zentrale Fehlerbehandlung. Fehler (Exceptions) in einer Promise-Kette werden durch die Kette “durchgereicht”, bis sie von einem .catch()-Handler abgefangen werden:

holeBenutzerId()
  .then(id => {
    if (id <= 0) {
      throw new Error('Ungültige Benutzer-ID');
    }
    return holeBenutzerDetails(id);
  })
  .then(benutzer => {
    console.log('Benutzerdetails:', benutzer);
    return holeBenutzerPosts(benutzer);
  })
  .catch(fehler => {
    // Fängt Fehler aus ALLEN vorherigen Promises ab
    console.error('Fehler in der Promise-Kette:', fehler);
  })
  .then(() => {
    console.log('Diese Ausführung erfolgt auch nach einem abgefangenen Fehler');
  });

Wenn ein Fehler in einem beliebigen .then()-Block auftritt (entweder durch das Werfen einer Exception oder durch die Ablehnung eines zurückgegebenen Promise), wird die Verarbeitung zum nächsten .catch()-Handler weitergeleitet.

19.6.1 Präzise Fehlerbehandlung

In komplexeren Szenarien kann es sinnvoll sein, verschiedene Fehler unterschiedlich zu behandeln:

function verarbeiteDaten() {
  return holeDatenVomServer('https://api.beispiel.de/daten')
    .then(daten => {
      // Datenverarbeitung
      return transformiereDaten(daten);
    })
    .catch(fehler => {
      if (fehler.name === 'NetworkError') {
        console.error('Netzwerkfehler aufgetreten, versuche lokalen Cache');
        return holeDatenAusCache();
      }
      
      if (fehler.name === 'ValidationError') {
        console.error('Validierungsfehler:', fehler.message);
        return []; // Leeres Array als Fallback
      }
      
      // Andere Fehler weiterleiten
      throw fehler;
    })
    .then(daten => {
      // Weitere Verarbeitung mit den Daten (entweder vom Server oder aus dem Cache)
      return berechneFinalenWert(daten);
    })
    .catch(fehler => {
      // Fängt alle nicht behandelten Fehler ab
      console.error('Unerwarteter Fehler:', fehler);
      return null; // Fallback-Rückgabewert
    });
}

19.7 Parallele Ausführung mit Promise-Methoden

Promise bietet statische Methoden zur Verarbeitung mehrerer Promises:

19.7.1 Promise.all() - Alle oder keiner

const promise1 = holeDatenVomServer('https://api.beispiel.de/benutzer');
const promise2 = holeDatenVomServer('https://api.beispiel.de/produkte');
const promise3 = holeDatenVomServer('https://api.beispiel.de/bestellungen');

Promise.all([promise1, promise2, promise3])
  .then(([benutzer, produkte, bestellungen]) => {
    console.log('Alle Daten erfolgreich geladen:');
    console.log('Benutzer:', benutzer);
    console.log('Produkte:', produkte);
    console.log('Bestellungen:', bestellungen);
  })
  .catch(fehler => {
    // Wird aufgerufen, sobald ein Promise fehlschlägt
    console.error('Mindestens eine Anfrage ist fehlgeschlagen:', fehler);
  });

Promise.all() nimmt ein Array von Promises entgegen und gibt ein Promise zurück, das: - erfüllt wird mit einem Array aller Ergebnisse, wenn alle Promises erfolgreich sind - abgelehnt wird mit dem ersten aufgetretenen Fehler, wenn mindestens ein Promise fehlschlägt

19.7.2 Promise.race() - Der Schnellste gewinnt

const serverA = holeDatenVomServer('https://serverA.beispiel.de/daten');
const serverB = holeDatenVomServer('https://serverB.beispiel.de/daten');

Promise.race([serverA, serverB])
  .then(ergebnis => {
    console.log('Der schnellere Server hat geantwortet:', ergebnis);
  })
  .catch(fehler => {
    console.error('Der schnellere Server hatte einen Fehler:', fehler);
  });

Promise.race() nimmt ein Array von Promises entgegen und gibt ein Promise zurück, das aufgelöst oder abgelehnt wird, sobald einer der Promises aufgelöst oder abgelehnt wird.

19.7.3 Promise.allSettled() (ES2020)

const anfragen = [
  holeDatenVomServer('https://api.beispiel.de/benutzer'),
  holeDatenVomServer('https://api.beispiel.de/produkte'),
  holeDatenVomServer('https://fehlerhafte-url.de/daten')
];

Promise.allSettled(anfragen)
  .then(ergebnisse => {
    ergebnisse.forEach((ergebnis, index) => {
      if (ergebnis.status === 'fulfilled') {
        console.log(`Anfrage ${index + 1} erfolgreich:`, ergebnis.value);
      } else {
        console.log(`Anfrage ${index + 1} fehlgeschlagen:`, ergebnis.reason);
      }
    });
  });

Promise.allSettled() wartet, bis alle Promises abgeschlossen sind (erfolgreich oder fehlgeschlagen) und liefert für jedes Promise ein Statusobjekt.

19.7.4 Promise.any() (ES2021)

const server1 = holeDatenVomServer('https://server1.beispiel.de/daten');
const server2 = holeDatenVomServer('https://server2.beispiel.de/daten');
const server3 = holeDatenVomServer('https://server3.beispiel.de/daten');

Promise.any([server1, server2, server3])
  .then(erstesErfolgreichesErgebnis => {
    console.log('Mindestens ein Server hat erfolgreich geantwortet:', erstesErfolgreichesErgebnis);
  })
  .catch(fehler => {
    console.error('Alle Server haben versagt:', fehler);
    // fehler ist ein AggregateError mit einer .errors-Eigenschaft
    fehler.errors.forEach((e, i) => {
      console.error(`Fehler von Server ${i + 1}:`, e);
    });
  });

Promise.any() gibt das erste erfolgreich aufgelöste Promise zurück und ignoriert Fehler, solange mindestens ein Promise erfolgreich ist. Wenn alle Promises fehlschlagen, gibt es einen AggregateError zurück.

19.8 Promise-Erstellung und -Transformation

19.8.1 Einfache Promise-Hilfsfunktionen

// Ein sofort aufgelöstes Promise erstellen
const sofortAufgelöst = Promise.resolve('Direktes Ergebnis');

// Ein sofort abgelehntes Promise erstellen
const sofortAbgelehnt = Promise.reject(new Error('Direkter Fehler'));

// Eine Verzögerung mit einem Promise umsetzen
function warte(millisekunden) {
  return new Promise(resolve => {
    setTimeout(resolve, millisekunden);
  });
}

// Verwendung:
warte(2000)
  .then(() => console.log('2 Sekunden sind vergangen'));

19.8.2 Umwandlung von Callback-basierten Funktionen zu Promises (Promisification)

// Callback-basierte Funktion
function traditionelleAsyncFunktion(wert, callback) {
  setTimeout(() => {
    if (wert < 0) {
      callback(new Error('Negativer Wert nicht erlaubt'));
    } else {
      callback(null, wert * 2);
    }
  }, 1000);
}

// Promise-Wrapper (Promisification)
function promisifiedFunktion(wert) {
  return new Promise((resolve, reject) => {
    traditionelleAsyncFunktion(wert, (fehler, ergebnis) => {
      if (fehler) {
        reject(fehler);
      } else {
        resolve(ergebnis);
      }
    });
  });
}

// Verwendung der promisifizierten Funktion
promisifiedFunktion(10)
  .then(ergebnis => console.log('Ergebnis:', ergebnis))
  .catch(fehler => console.error('Fehler:', fehler));

19.9 Promise-Verschränkungen und fortgeschrittene Muster

19.9.1 Dynamisches Promise-Chaining

// Ein Array von IDs
const benutzerIds = [1, 2, 3, 4, 5];

// Sequentielles Verarbeiten eines Arrays mit Promises
benutzerIds
  .reduce((promiseKette, id) => {
    return promiseKette
      .then(akkumulator => {
        return holeDatenVomServer(`https://api.beispiel.de/benutzer/${id}`)
          .then(benutzerDaten => {
            akkumulator.push(benutzerDaten);
            return akkumulator;
          });
      });
  }, Promise.resolve([]))
  .then(alleBenutzerDaten => {
    console.log('Alle Benutzerdaten sequentiell geladen:', alleBenutzerDaten);
  })
  .catch(fehler => {
    console.error('Fehler beim Laden der Benutzerdaten:', fehler);
  });

19.9.2 Paralleles Mapping mit Promises

function mapAsync(array, asyncFunktion) {
  return Promise.all(array.map(item => asyncFunktion(item)));
}

// Beispielverwendung
mapAsync(benutzerIds, id => holeDatenVomServer(`https://api.beispiel.de/benutzer/${id}`))
  .then(benutzerArray => {
    console.log('Alle Benutzer parallel geladen:', benutzerArray);
  })
  .catch(fehler => {
    console.error('Fehler beim parallelen Laden:', fehler);
  });

19.9.3 Limitierung paralleler Anfragen

async function limitParallelAsync(array, asyncFunktion, limit) {
  const results = [];
  const executing = [];

  for (const [index, item] of array.entries()) {
    const p = Promise.resolve().then(() => asyncFunktion(item, index, array));
    results.push(p);
    
    if (limit <= array.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }
  
  return Promise.all(results);
}

// Beispielverwendung: Maximal 3 parallele Anfragen
limitParallelAsync(benutzerIds, id => holeDatenVomServer(`https://api.beispiel.de/benutzer/${id}`), 3)
  .then(ergebnisse => {
    console.log('Alle Anfragen abgeschlossen:', ergebnisse);
  });

19.10 Timeout für Promises

Ein häufig benötigtes Muster ist die Implementierung eines Timeouts für Promises:

function mitTimeout(promise, millisekunden) {
  const timeout = new Promise((_, reject) => {
    const id = setTimeout(() => {
      clearTimeout(id);
      reject(new Error(`Timeout nach ${millisekunden} ms`));
    }, millisekunden);
  });

  return Promise.race([promise, timeout]);
}

// Verwendung
mitTimeout(holeDatenVomServer('https://api.beispiel.de/daten'), 5000)
  .then(daten => {
    console.log('Daten rechtzeitig erhalten:', daten);
  })
  .catch(fehler => {
    if (fehler.message.includes('Timeout')) {
      console.error('Die Anfrage hat zu lange gedauert.');
    } else {
      console.error('Ein anderer Fehler ist aufgetreten:', fehler);
    }
  });

19.11 Promises und Browser-APIs

Viele moderne Browser-APIs geben direkt Promises zurück:

// Fetch API
fetch('https://api.beispiel.de/daten')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP-Fehler: ${response.status}`);
    }
    return response.json();
  })
  .then(daten => {
    console.log('Daten:', daten);
  })
  .catch(fehler => {
    console.error('Fehler beim Abrufen der Daten:', fehler);
  });

// Cache API
caches.open('mein-cache')
  .then(cache => {
    return cache.add('/styles.css');
  })
  .then(() => {
    console.log('CSS-Datei erfolgreich gecacht');
  });

// Web Animation API
document.querySelector('.element').animate(
  [{ opacity: 0 }, { opacity: 1 }], 
  { duration: 1000 }
).finished
  .then(() => {
    console.log('Animation abgeschlossen');
  });

19.12 Promise-basierte Redesigns älterer APIs

Viele ältere APIs wurden mit Promise-basierten Versionen neu gestaltet:

// IndexedDB via Promise-Wrapper
const dbPromise = window.indexedDB.open('meine-datenbank', 1);

dbPromise
  .then(db => {
    const tx = db.transaction('kunden', 'readonly');
    const store = tx.objectStore('kunden');
    return store.get(42);
  })
  .then(kunde => {
    console.log('Kunde gefunden:', kunde);
  })
  .catch(fehler => {
    console.error('Fehler beim Zugriff auf die Datenbank:', fehler);
  });

19.13 Promise-Anti-Patterns und häufige Fehler

19.13.1 Fehler 1: Vergessen der Fehlerbehandlung

// Schlecht - keine Fehlerbehandlung
holeDatenVomServer('https://api.beispiel.de/daten')
  .then(daten => {
    verarbeiteDaten(daten);
  });
  // Fehler werden ignoriert und "verschluckt"

// Besser
holeDatenVomServer('https://api.beispiel.de/daten')
  .then(daten => {
    verarbeiteDaten(daten);
  })
  .catch(fehler => {
    console.error('Fehler beim Laden oder Verarbeiten der Daten:', fehler);
  });

19.13.2 Fehler 2: Promise-Hell (neue Form der Callback-Hölle)

// Schlecht - "Promise-Hell"
holeDatenVomServer('https://api.beispiel.de/daten')
  .then(daten => {
    return holeDatenVomServer(`https://api.beispiel.de/details/${daten.id}`)
      .then(details => {
        return holeDatenVomServer(`https://api.beispiel.de/extras/${details.extraId}`)
          .then(extras => {
            // Verschachtelte then-Callbacks
            return { daten, details, extras };
          });
      });
  })
  .catch(fehler => {
    console.error('Fehler:', fehler);
  });

// Besser - flache Promise-Kette
holeDatenVomServer('https://api.beispiel.de/daten')
  .then(daten => {
    return holeDatenVomServer(`https://api.beispiel.de/details/${daten.id}`)
      .then(details => {
        return { daten, details };
      });
  })
  .then(({ daten, details }) => {
    return holeDatenVomServer(`https://api.beispiel.de/extras/${details.extraId}`)
      .then(extras => {
        return { daten, details, extras };
      });
  })
  .catch(fehler => {
    console.error('Fehler:', fehler);
  });

19.13.3 Fehler 3: Nicht-Rückgabe von Promises in einer Kette

// Schlecht - Promise-Rückgabe fehlt
benutzerEinloggen()
  .then(benutzer => {
    // Promise-Rückgabe fehlt, die Kette wird unterbrochen
    holeDatenVomServer(`https://api.beispiel.de/profil/${benutzer.id}`);
  })
  .then(profil => {
    // 'profil' ist undefined, weil das vorherige Promise nicht zurückgegeben wurde
    console.log(profil.name); // TypeError
  });

// Besser
benutzerEinloggen()
  .then(benutzer => {
    // Promise wird korrekt zurückgegeben
    return holeDatenVomServer(`https://api.beispiel.de/profil/${benutzer.id}`);
  })
  .then(profil => {
    // 'profil' enthält nun die korrekten Daten
    console.log(profil.name);
  });