Asynchrones JavaScript

Lerne, wie JavaScript zeitaufwendige Aufgaben verarbeitet, ohne die Seite einzufrieren. Verfolge den Ablauf der Ereignisschleife, um die Ausführungsreihenfolge von Code vorherzusagen, plane Aufgaben mit Timern und schreibe asynchronen Code in allen drei Formen: mit Callbacks, Promises und async-Funktionen mit await. Behandle Fehler mit catch, try und finally und führe unabhängige Aufgaben mit Promise.all und den anderen Promise-Kombinatoren gleichzeitig aus.

AnfängerEinheit 7 von 9 in Kurs: JavaScript – DOM, Ereignisse und asynchroner Code

Was du lernst

  1. Unterscheide synchronen und asynchronen Code, verfolge die Ereignisschleife anhand des Aufrufstapels und ihrer Warteschlangen und plane Aufgaben mit Timern.
    • Unterscheide synchronen und asynchronen Code
      Erkläre, warum JavaScript jeweils einen Teil deines Codes ausführt, warum lang laufender Code eine Seite einfriert und wie asynchrone Operationen einem Programm ermöglichen, während des Wartens weiterzuarbeiten.
    • Verfolge den Ablauf der Ereignisschleife
      Verfolge, wie Aufrufstapel, Task-Warteschlange und Microtask-Warteschlange die Ausführungsreihenfolge bestimmen, und sage die Ausgabe von Code voraus, der Timer und Promises kombiniert.
    • Plane die Ausführung von Code mit Timern
      Führe Code mit setTimeout und setInterval einmal nach einer Verzögerung oder wiederholt aus, brich Timer ab und erkläre, warum die Verzögerung eines Timers eine Mindestwartezeit und kein exakter Zeitpunkt ist.
  2. Übergib Funktionen an asynchrone Operationen, melde Fehler nach der Error-first-Konvention und erkenne, warum tief verschachtelte Callbacks schwer zu lesen und zu warten sind.
    • Übergib Callbacks an asynchrone Operationen
      Schreibe Funktionen, die einen Callback entgegennehmen, unterscheide synchrone und asynchrone Callbacks und leite ein asynchrones Ergebnis an den Code weiter, der es braucht.
    • Behandle Fehler in Callbacks und vermeide die Callback-Hölle
      Nutze die Error-first-Callback-Konvention, erkläre, warum try und catch einen später in einem Callback ausgelösten Fehler nicht abfangen können, und mache tief verschachtelten Callback-Code flacher.
  3. Erstelle Promises, reagiere mit then, catch und finally auf ihre Ergebnisse und verkette asynchrone Schritte zu einer lesbaren Abfolge.
    • Erkläre Promise-Zustände und erstelle ein Promise
      Beschreibe die möglichen Zustände eines Promises, erstelle Promises mit dem Promise-Konstruktor und bette einen Timer in ein Promise ein.
    • Reagiere mit then, catch und finally auf Promises
      Registriere Handler, die laufen, wenn ein Promise erfüllt, abgelehnt oder abgeschlossen wird, und nutze sie, um eine Seite zum richtigen Zeitpunkt zu aktualisieren.
    • Verkette Promises zu einer Abfolge
      Gib Werte und Promises aus Handlern zurück, um flache Ketten abhängiger Schritte aufzubauen, und vermeide fehlende Rückgaben und unnötige Verschachtelung.
  4. Schreibe async-Funktionen, halte sie mit await an, bis Promises abgeschlossen sind, schreibe Promise-Ketten und die Nutzung von Callback-APIs in diesem Stil um und vermeide häufige Fehler in asynchronem Code.
    • Schreibe async-Funktionen und nutze await
      Deklariere async-Funktionen, erkläre, was sie zurückgeben, und halte eine Funktion mit await an, bis ein Promise abgeschlossen ist, ohne die Seite zu blockieren.
    • Schreibe Promise-Ketten und die Nutzung von Callback-APIs mit async und await um
      Wandle Promise-Ketten in async-Funktionen um, bette Funktionen mit Callbacks in Promises ein, damit du mit await auf sie warten kannst, und wähle einen klaren Stil.
    • Vermeide häufige Fehler mit async und await
      Erkenne und behebe vergessene awaits, asynchrone Callbacks in forEach, als Bedingungen verwendete Promises und asynchrone Aufrufe, deren Fehler unbehandelt bleiben.
  5. Fange Fehler in Promise-Ketten und mit try und catch ab und setze die Verarbeitung nach Möglichkeit fort, räume mit finally auf und stelle sicher, dass Fehler nicht unbemerkt bleiben.
    • Behandle Ablehnungen in Promise-Ketten
      Verfolge, wie eine Ablehnung eine Promise-Kette durchläuft, setze die Verarbeitung nach Möglichkeit fort oder gib die Ablehnung weiter und platziere catch-Handler so, dass sie die richtigen Fehler behandeln.
    • Nutze try, catch und finally mit await
      Nutze Error-Objekte und die Anweisung try, catch und finally, um Fehler erwarteter Promises zu behandeln, zuverlässig aufzuräumen und sinnvolle Rückfalllösungen bereitzustellen.
    • Mache Fehler sichtbar und setze die Verarbeitung nach Möglichkeit sinnvoll fort
      Verhindere unbehandelte Promise-Ablehnungen, vermeide das Verschlucken von Fehlern, ergänze Fehler um Kontext und entscheide zwischen Wiederholungsversuch, Rückfalllösung und einer Information an den Nutzer.
  6. Starte unabhängige Aufgaben gleichzeitig, warte gemeinsam auf sie und wähle den Promise-Kombinator für Situationen, in denen alle Ergebnisse, Teilergebnisse, das erste abgeschlossene Ergebnis oder der erste Erfolg gefragt sind.
    • Starte Aufgaben gemeinsam und warte mit Promise.all auf ihre Ergebnisse
      Führe unabhängige asynchrone Aufgaben gleichzeitig aus, kombiniere ihre Ergebnisse mit Promise.all und erkläre das zeitliche Verhalten und das Verhalten bei Fehlern.
    • Wähle zwischen Promise.allSettled, Promise.race und Promise.any
      Nutze allSettled, wenn jedes Ergebnis zählt, race für die erste abgeschlossene Aufgabe, etwa bei einer Zeitüberschreitung, und any für den ersten Erfolg; vergleiche außerdem alle vier Kombinatoren.

Asynchrones JavaScript einfach erklärt

6 Kapitel

1

Erkläre, wie JavaScript asynchronen Code ausführt

JavaScript muss oft warten, während jemand klickt oder ein Timer herunterläuft. Währenddessen soll die Seite reaktionsfähig bleiben. Verschaffe dir ein Bild davon, was jetzt läuft, was in der Umgebung wartet und was für später in eine Warteschlange eingereiht wird. Nutze dieses Bild anschließend, um Konsolenausgaben nachzuvollziehen und Timer einzuplanen. Entscheidend ist, zu verfolgen, wann eine Funktion aufgerufen wird, und nicht nur, wo ihr Name im Code steht.

1.1 Unterscheide synchronen und asynchronen Code

Synchroner Code beendet die aktuelle Anweisung, bevor er zur nächsten übergeht. Das JavaScript deiner Seite läuft auf einem Hauptthread, sodass zwei Teile dieses Codes nicht gleichzeitig ausgeführt werden. Das ist leicht nachzuvollziehen, bis eine Anweisung eine Aufgabe startet, deren Ergebnis erst später eintrifft.

Jetzt starten, das Ergebnis später verarbeiten

Eine asynchrone Operation startet jetzt und liefert ihr Ergebnis anschließend über einen Callback oder ein Promise. Der Browser oder Node.js ist die Hostumgebung. Sie übernimmt das Warten, während dein Programm weiterläuft. Timer, Nutzereingaben und das Laden von Bildern funktionieren auf diese Weise.

console.log("Opening");
setTimeout(() => console.log("Ready"), 700);
console.log("Waiting");

Lies den Aufruf von setTimeout als „Registriere diese Funktion“. Der Aufruf kehrt zurück, ohne 700 ms zu warten. Das aktuelle Skript gibt daher zuerst „Opening“ und „Waiting“ aus. Nach etwa 700 ms kann der Callback laufen und „Ready“ ausgeben. Die genaue Verzögerung hängt auch davon ab, wann der Hauptthread frei wird.

Hier gibt es zwei verschiedene Vorgänge: Die Umgebung überwacht den Timer und JavaScript ruft schließlich die Funktion auf, die das Ergebnis ausgibt. Das Warten überschneidet sich mit anderer Arbeit; der Callback läuft weiterhin auf dem Hauptthread.

Warum eine Seite einfriert

Eine lange synchrone Schleife hält den Thread beschäftigt. Während sie läuft, können Klick-Handler und Timer-Callbacks nicht ausgeführt werden und die Seite kann ihre Anzeige über diesen Thread nicht aktualisieren. Ein registrierter Timer mit null Verzögerung kann die Schleife nicht unterbrechen.

Angenommen, ein Timer wird bereit, während eine Berechnung noch läuft. Die Umgebung reiht seinen Callback in eine Warteschlange ein. Erst das Ende der Berechnung gibt den Thread frei; der Ablauf der Timerfrist allein kann das nicht. Auch ein Callback, der eine weitere lange Berechnung ausführt, kann die Seite einfrieren.

Um die Auswirkung einer Berechnung auf die Reaktionsfähigkeit zu verringern, zerlege sie in kleinere Teile, zwischen denen andere Tasks laufen können, oder lagere geeignete Arbeit an einen Webworker aus, einen separaten Hintergrundthread des Browsers. Setzt du eine große Schleife lediglich in eine async-Funktion, bleibt sie auf dem Hauptthread.

Drei Wege, ein Ergebnis zu erhalten

Mit einem Callback wird die nächste Aktion als Funktion übergeben. Ein Promise steht für ein späteres Ergebnis und lässt dich Handler für Erfolg und Fehler registrieren. Eine async-Funktion arbeitet mit Promises, während await ihre eigene Ausführung an einer klaren Stelle anhalten kann.

Diese Formen bestehen nebeneinander. Ein Timer nimmt weiterhin einen Callback entgegen, auch wenn der umgebende Code Promises nutzt. Eine async-Funktion kann auf ein Promise warten, das ein Callback-Wrapper erzeugt. Wähle den Stil, mit dem sich der Weg des Ergebnisses leicht verfolgen lässt.

Markiere beim Lesen asynchronen Codes zuerst die Anweisungen, die während des aktuellen Aufrufs laufen. Markiere danach die Funktionen, die für später registriert werden. Eine Zeile innerhalb eines registrierten Callbacks läuft nicht schon deshalb, weil die Ausführung die Zeile erreicht hat, die diese Funktion erstellt.

Wichtige Erkenntnisse

  • Synchrones JavaScript belegt den Hauptthread, bis die aktuelle Arbeit beendet ist.
  • Die Hostumgebung übernimmt das Warten und plant Code zur Verarbeitung des Ergebnisses ein.
  • Asynchrone Callbacks laufen später auf dem Hauptthread.
  • Auch eine async-Funktion kann die Seite mit einer langen Berechnung blockieren.

1.2 Verfolge den Ablauf der Ereignisschleife

Die Ereignisschleife koordiniert ausführungsbereite Aufgaben. Um die Ausgabe vorherzusagen, halte drei Dinge auseinander: die jetzt laufenden Funktionen, als Tasks wartende Callbacks und als Microtasks wartende Promise-bezogene Aufgaben.

Verfolge Stapel und Warteschlangen

Der Aufrufstapel enthält aktive Funktionsaufrufe. Beim Aufruf einer Funktion wird ein Stapelrahmen daraufgelegt; bei der Rückkehr wird er entfernt. Die aktuelle Funktion läuft, bis sie zurückkehrt oder eine andere Funktion aufruft.

Ein Task kann ein Skript, ein Timer-Callback oder ein Klick-Handler sein. Tasks laufen einzeln. Browser können mehrere Task-Warteschlangen haben. Gehe deshalb nicht davon aus, dass alle Aufgabenquellen eine einzige gemeinsame Warteschlange nach dem Prinzip „zuerst hinein, zuerst heraus“ nutzen.

Die Microtask-Warteschlange enthält Promise-Handler aus then, catch und finally, Fortsetzungen nach await und an queueMicrotask übergebene Callbacks. An einem Microtask-Prüfpunkt nach der aktuellen Arbeit wird die Warteschlange vor dem nächsten Task geleert. Auch Microtasks, die währenddessen hinzukommen, laufen vor dem nächsten Task.

Verfolge ein Skript
console.log("begin");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("finish");
  1. Das Skript gibt „begin“ aus.
  2. Es registriert einen Timer-Callback; es ruft ihn nicht auf.
  3. Es reiht den Handler des erfüllten Promises als Microtask ein.
  4. Es gibt „finish“ aus und beendet die aktuelle Arbeit.
  5. Der Microtask gibt „promise“ aus.
  6. Der Timer-Task gibt „timer“ aus.

Auch ein bereits erfülltes Promise plant seinen Handler für später ein. Auch eine Verzögerung von 0 ms plant einen Timer-Task ein.

Berücksichtige unterwegs hinzugefügte Aufgaben
Promise.resolve()
  .then(() => console.log("first"))
  .then(() => console.log("third"));
queueMicrotask(() => console.log("second"));
console.log("now");

Das Skript gibt „now“ aus. Die Microtask-Warteschlange enthält zunächst den ersten Promise-Handler und dann den queueMicrotask-Callback. Wenn der erste Handler „first“ ausgibt und zurückkehrt, macht er den nächsten Promise-Handler bereit. Dieser neue Handler wird am Ende der Warteschlange eingereiht. Die restliche Ausgabe lautet „second“, dann „third“.

Vollständige Ausführung und Reaktionsfähigkeit

Sobald ein Task oder Microtask beginnt, kann asynchron eingereihte Arbeit ihn nicht unterbrechen. Dieses Run-to-Completion-Prinzip lässt dich eine lokale Änderung nachvollziehen, ohne dass ein Timer plötzlich mitten in die Funktion eingreift.

Nach den Microtasks kann der Browser bei einer Gelegenheit zum Rendern den Bildschirm aktualisieren. Er muss nicht nach jedem Task neu zeichnen. Ein endloser Strom von Microtasks kann sowohl spätere Tasks als auch das Rendern verhindern: Jeder neue Microtask hält das aktuelle Leeren der Warteschlange aufrecht. Wenn du Arbeit an einen Microtask verschiebst, ist daher nicht garantiert, dass die Seite Gelegenheit erhält zu reagieren.

Schreibe für eine Ablaufverfolgung zuerst die unmittelbare Ausgabe auf, leere die Microtask-Warteschlange der Reihe nach und gehe erst dann zu bereiten Timer-Tasks über. Füge neu eingeplante Arbeit der passenden Warteschlange hinzu, sobald der Code sie tatsächlich einplant.

Verfolge einen Timer, der von einem Microtask erstellt wird
setTimeout(() => console.log("outer timer"), 0);
Promise.resolve().then(() => {
  console.log("job one");
  setTimeout(() => console.log("inner timer"), 0);
}).then(() => console.log("job two"));
console.log("script");

Das Skript registriert den äußeren Timer und den ersten Promise-Handler und gibt dann „script“ aus. Der erste Microtask gibt „job one“ aus und registriert den inneren Timer. Die Rückkehr aus diesem Handler reiht „job two“ ein, was ebenfalls vor einem Timer-Task läuft. Die Ausgabe endet mit „outer timer“, dann „inner timer“: Beide Timer haben dieselbe Verzögerung und der äußere wurde zuerst registriert.

Wird ein Timer während eines Microtasks erstellt, macht das seinen Callback nicht zu einem Microtask. Ordne die Aufgabe anhand der API ein, die sie entgegennimmt und einplant, und füge sie dann der passenden Warteschlange hinzu.

Wichtige Erkenntnisse

  • Der Aufrufstapel hält aktive Aufrufe fest.
  • Promise-Handler und Fortsetzungen nach await laufen als Microtasks.
  • Führe neu hinzugefügte Microtasks aus, bevor du den nächsten Task nimmst.
  • Eingereihte Callbacks können gerade laufendes JavaScript nicht unterbrechen.
  • Rendern ist nach den Microtasks möglich, aber nicht nach jedem Task garantiert.

1.3 Plane die Ausführung von Code mit Timern

Timer planen Callbacks für später ein. Sie messen eine Verzögerung in Millisekunden: 1000 ms sind 1 s. Die Verzögerung legt fest, ab wann ein Callback ausführungsbereit werden kann; andere Aufgaben und Beschränkungen des Browsers können seine Ausführung weiter verzögern.

Einen Aufruf einplanen und abbrechen
function remind(topic) {
  console.log("Review " + topic);
}
const reminder = setTimeout(remind, 2000, "promises");
// To cancel before it runs:
clearTimeout(reminder);

Zusätzliche Argumente nach der Verzögerung werden an den Callback übergeben. Eine fehlende Verzögerung zählt als 0. Speichere die zurückgegebene Timerkennung, falls du den Timer abbrechen musst. Browser geben eine numerische Kennung zurück; Node.js gibt ein Timeout-Objekt zurück. Beide funktionieren mit clearTimeout. Das Abbrechen eines abgelaufenen oder nicht existierenden Timers hat keine Wirkung.

Übergib eine Funktion: setTimeout(remind, 2000, "promises"). Schreibst du setTimeout(remind("promises"), 2000), wird remind sofort aufgerufen und sein Rückgabewert an den Timer übergeben. Eine Pfeilfunktion ist eine weitere Möglichkeit, einen Aufruf mit Argumenten zu verzögern: setTimeout(() => remind("promises"), 2000).

Mit einem Intervalltimer oder einem weiteren Timeout wiederholen
let ticks = 0;
const clock = setInterval(() => {
  ticks++;
  console.log("Tick " + ticks);
  if (ticks === 3) clearInterval(clock);
}, 1000);

Der erste Durchlauf erfolgt nach etwa einer Sekunde. Der dritte Callback stoppt weitere Durchläufe. Bewahre die Kennung des Intervalltimers auf und verwende clearInterval, wenn die wiederholte Aufgabe enden soll.

Stattdessen kann ein Callback seinen nächsten Timeout selbst einplanen. Ruft er setTimeout am Ende seiner synchronen Arbeit auf, beginnt die nächste Verzögerung nach dieser Arbeit. Das ergibt eine Pause zwischen den Ausführungen und erlaubt jedes Mal eine andere Verzögerung. Ein Intervalltimer fordert Ausführungen nach einem festen Zeitplan an; bei langsamer Arbeit kann wenig Pause bleiben. Startet ein Durchlauf asynchrone Arbeit, warte auch auf deren Ende, bevor du den nächsten Durchlauf einplanst, falls eine Überschneidung problematisch wäre.

Grenzen der zeitlichen Steuerung

Ein beschäftigter Thread verzögert bereite Callbacks. Browser drosseln Timer in Hintergrund-Tabs außerdem oft auf etwa eine Prüfung pro Sekunde oder seltener. Ist die Verschachtelungstiefe von Timern größer als fünf, hebt die HTML-Timerregel Verzögerungen unter 4 ms auf 4 ms an. Auch bei sehr großen Verzögerungen ist Vorsicht nötig: Die übliche vorzeichenbehaftete 32-Bit-Grenze liegt bei 2147483647 ms, etwa 24,8 Tagen; Werte darüber können einen Überlauf verursachen und viel früher auslösen.

Den richtigen Schleifenwert erfassen
for (let i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 100);
}

Jeder let-Schleifendurchlauf hat eine eigene Bindung, daher lautet die Ausgabe „1“, „2“, „3“. Mit var teilen sich alle Callbacks dieselbe Variable. Nach der Schleife steht sie auf 4, sodass jeder „4“ ausgibt.

Auf eine ruhige Phase warten

Debouncing bricht bei jedem neuen Ereignis einen ausstehenden Timeout ab. Ein neuer Timeout plant die Aktion dann nach dem letzten Ereignis ein. Bei einer Verzögerung von 300 ms verschiebt eine Eingabe alle 100 ms die Aktion immer weiter; sie läuft etwa 300 ms nach Ende der Eingabe. Speichere die Kennung außerhalb des Handlers, damit der nächste Aufruf den vorherigen Timer abbrechen kann.

Einen Debounce-Handler erstellen
let pendingSave;
function scheduleSave(text) {
  clearTimeout(pendingSave);
  pendingSave = setTimeout(() => {
    console.log("Saved: " + text);
  }, 300);
}
scheduleSave("n");
scheduleSave("no");
scheduleSave("note");

Diese Aufrufe finden im selben Skript statt. Jeder bricht den vorherigen Timer ab, bevor er die Kennung ersetzt. Daher wird nach etwa 300 ms nur „Saved: note“ ausgegeben. Der letzte Callback erfasst den Textparameter dieses Aufrufs. Die Kennung liegt außerhalb der Funktion, damit jeder Aufruf den zuvor eingeplanten Timeout abbrechen kann.

Würde die Kennung innerhalb jedes Aufrufs neu deklariert, hätte der nächste Aufruf keinen Zugriff auf die Kennung des vorherigen Timers. Würde ein Handler einen Timer einplanen, ohne den vorherigen abzubrechen, würden alle drei Meldungen ausgegeben. Debouncing benötigt sowohl die gemeinsam genutzte Kennung als auch den Abbruch vor dem Ersetzen.

Wichtige Erkenntnisse

  • Übergib eine Callback-Funktion und miss Verzögerungen in Millisekunden.
  • Nutze die zurückgegebene Kennung, um einen Timer abzubrechen.
  • Timerverzögerungen garantieren keine exakten Ausführungszeitpunkte.
  • Ein Intervalltimer wiederholt sich, bis er abgebrochen wird.
  • Nutze let für getrennte Schleifenbindungen.
  • Debouncing startet nach jedem Ereignis eine Verzögerung neu.
2

Schreibe und organisiere Code mit Callbacks

Mit einem Callback kann der aufrufende Code die nächste Aufgabe als Funktion bereitstellen. Diese einfache Idee unterstützt Timer, Ereignisse und viele ältere APIs. Hier schreibst du eine kleine Operation mit Callback, verfolgst das Ergebnis bis zu dem Code, der es benötigt, und gibst Fehler über einen Fehlerparameter weiter. Außerdem siehst du, wie abhängige Schritte verschachtelten Code erzeugen und warum eine einheitliche Fehlerbehandlung auf jeder Ebene wichtig ist.

2.1 Übergib Callbacks an asynchrone Operationen

Ein Callback ist eine Funktion, die du an eine andere Funktion übergibst, damit diese sie aufruft. Der Code des Callbacks beschreibt, was mit einem Ergebnis geschehen soll. Ob er während des aktuellen Aufrufs oder später läuft, hängt von der API ab, die ihn entgegennimmt.

Ein Callback kann synchron laufen
const doubled = [2, 5].map(n => n * 2);
console.log(doubled);

map ruft seinen Callback für jedes Element auf, bevor es das neue Array zurückgibt. Das Ergebnis ist [4, 10]. Die Pfeilfunktion ist ein Callback, aber dieser Code plant keine spätere Arbeit ein. Du kannst doubled in der nächsten Zeile verwenden, weil das Array bereits vollständig ist.

Ein verzögertes Ergebnis bereitstellen
function prepareLabel(name, callback) {
  setTimeout(() => {
    callback("Label: " + name);
  }, 200);
}
prepareLabel("Mira", label => {
  console.log(label);
});
console.log("Label requested");

prepareLabel startet einen Timer und kehrt zurück. Das Skript gibt „Label requested“ aus. Nach etwa 200 ms erstellt der Timer-Callback die Beschriftung und ruft den übergebenen Callback auf, der „Label: Mira“ ausgibt. Die Operation entscheidet, wann sie aufruft; der aufrufende Code entscheidet, was mit dem Ergebnis geschieht.

Abhängige Arbeit dort platzieren, wo das Ergebnis vorliegt
let label;
prepareLabel("Mira", value => {
  label = value;
});
console.log(label);

Dies gibt undefined aus: Die Zuweisung ist noch nicht erfolgt. Verschiebe console.log(value) in den Callback. Würdest du vor dem Lesen von label einen längeren, unabhängigen Timeout einfügen, würdest du nur abschätzen, wann das Ergebnis verfügbar sein wird; mit dem Ergebnis-Callback knüpfst du die Arbeit an den tatsächlichen Abschluss.

Auch ein return innerhalb eines verzögerten Callbacks kann den Wert nicht aus der äußeren Funktion zurückgeben. Der äußere Aufruf ist bereits beendet. Im Beispiel würde return "Label: Mira" innerhalb des Timer-Callbacks an die Timerverwaltung zurückgeben; der Aufrufer von prepareLabel erhielte die verzögerte Beschriftung nicht.

Die nächste Aktion übergeben, ohne sie aufzurufen

Ein benannter Callback funktioniert ebenfalls:

function showLabel(label) {
  console.log(label);
}
prepareLabel("Mira", showLabel);

showLabel ist der Funktionswert. showLabel() würde die Funktion sofort ohne Beschriftung aufrufen und stattdessen ihren Rückgabewert übergeben. Nutze einen Pfeilfunktions-Wrapper, wenn die spätere Aktion ein bestimmtes Argument oder mehrere Anweisungen benötigt.

Bei zwei abhängigen asynchronen Schritten startest du den zweiten im Callback des ersten Schritts. Übergib zum Beispiel nach dem Erhalt eines Nutzers diesen Nutzer an eine Operation, die seine Einstellungen vorbereitet. Würdest du beide außerhalb ihrer Callbacks starten, entstünde diese Abhängigkeit nicht.

Die empfangende Funktion steuert, wie oft sie den Callback aufruft. Manche Callbacks, etwa Event-Handler, werden absichtlich häufig aufgerufen. Eine Operation mit einem Ergebnis sollte ihren Ergebnis-Callback einmal aufrufen. Ein Promise gibt einer solchen Operation ein Ergebnis, das nur einmal abgeschlossen werden kann.

Wichtige Erkenntnisse

  • Die API, die einen Callback aufruft, bestimmt seinen Ausführungszeitpunkt.
  • map nutzt synchrone Callbacks; ein Timer-Callback läuft später.
  • Nutze ein verzögertes Ergebnis innerhalb seines Callbacks oder in Code, den dieser aufruft.
  • Die Rückkehr aus einem Callback führt nicht zur Rückkehr aus seiner äußeren Funktion.

2.2 Behandle Fehler in Callbacks und vermeide die Callback-Hölle

Eine asynchrone Operation kann sowohl fehlschlagen als auch erfolgreich sein. Eine Callback-API braucht eine klare Möglichkeit, jedes Ergebnis zu melden, und eine klare Stelle, an der der aufrufende Code es behandelt.

Prüfe zuerst den Fehler

Die Error-first-Konvention nutzt callback(err, result). Ein erfolgreicher Aufruf übergibt null für den Fehler. Ein fehlgeschlagener Aufruf übergibt ein Error-Objekt und liefert kein nutzbares Ergebnis.

function readLabel(id, callback) {
  setTimeout(() => {
    if (id <= 0) {
      callback(new Error("Invalid label id"));
      return;
    }
    callback(null, { id: id, text: "Draft" });
  }, 200);
}
readLabel(4, (err, label) => {
  if (err) {
    console.log(err.message);
    return;
  }
  console.log(label.text);
});

Die bereitstellende Funktion stoppt, nachdem sie eine ungültige Kennung gemeldet hat, und kann daher nicht auch Erfolg melden. Der verarbeitende Code stoppt, nachdem er einen Fehler erkannt hat, und kann daher nicht versuchen, label.text aus einem fehlenden Ergebnis zu lesen. Mit der Kennung 4 lautet die Ausgabe „Draft“. Mit der Kennung 0 lautet sie „Invalid label id“.

Beide return-Anweisungen sind wichtig. Ohne das return im verarbeitenden Code könnte ein zweiter Fehler über einen nicht definierten Wert die hilfreiche ursprüngliche Meldung verdecken. Ohne das return in der bereitstellenden Funktion könnte eine Operation den Callback zweimal aufrufen. Bei einer API mit einem Ergebnis gilt: Melde genau ein Ergebnis.

Ein umgebendes try ist bereits beendet
try {
  setTimeout(() => {
    throw new Error("Late failure");
  }, 0);
} catch (err) {
  console.log("Handled");
}

Das try plant den Timer ein und endet. Der Callback löst in einem späteren Task eine Ausnahme aus, wenn dieses try nicht mehr aktiv ist. Daher wird „Handled“ nicht ausgegeben. Fange eine mögliche Ausnahme innerhalb des Callbacks ab oder gib einen Fehler über den Fehlerparameter der API weiter. Ein späterer Promise-Wrapper muss diesen Callback-Fehler ausdrücklich in eine Ablehnung umwandeln.

Abhängige Schritte lesbar halten

Callback-Code wird schwer nachvollziehbar, wenn jeder nächste Schritt innerhalb des Callbacks des vorherigen steht. Das Lesen eines Profils, dann seiner Projekte und dann der Aufgaben eines Projekts erzeugt mehrere Verschachtelungsebenen. Jede Operation benötigt außerdem eine eigene Fehlerprüfung. Dieses Muster wird oft Callback-Hölle oder Pyramide des Verderbens genannt.

Benannte Funktionen können diesen Schritten aussagekräftige Namen geben und die sichtbare Verschachtelung verringern. Melde Fehler durchgehend einheitlich: Ein späterer Schritt kann ebenso fehlschlagen wie der erste. Promises und async-Funktionen bieten eine flachere Möglichkeit, dieselbe Abfolge auszudrücken und dabei die Weitergabe von Fehlern zusammenhängend zu halten.

Nicht alle Callback-APIs verwenden einen Fehlerparameter. Manche Browserobjekte haben getrennte Ereignisse für Erfolg und Fehler. Setze bei einem Bild die Handler onload und onerror, bevor du src zuweist, damit beide Ergebnisse beim Start des Ladevorgangs einen Handler haben. Lies den Vertrag der API, bevor du annimmst, dass ein Parameter namens err existiert.

Wichtige Erkenntnisse

  • Ein Error-first-Callback erhält err vor dem Ergebnis.
  • Kehre nach dem Melden oder Behandeln eines Fehlers zurück.
  • Operationen mit einem Ergebnis sollten ihren Callback genau einmal aufrufen.
  • Ein äußeres try kann eine spätere Ausnahme aus einem Callback nicht abfangen.
  • Benannte Schritte oder Code mit Promises verringern tiefe Verschachtelung.
3

Erstelle und nutze Promises

Ein Promise gibt einer Operation ein gemeinsames Objekt für ihr späteres Ergebnis. Es macht Erfolg und Fehler explizit und lässt dich abhängige Schritte verbinden, ohne jeden im vorherigen zu verschachteln. Erstelle zunächst ein Promise um einen Timer. Registriere dann Handler, verfolge die neuen Promises, die sie zurückgeben, und prüfe, ob jeder asynchrone Schritt mit der Kette verbunden bleibt, die von ihm abhängt.

3.1 Erkläre Promise-Zustände und erstelle ein Promise

Ein Promise steht für ein späteres Ergebnis. Eine Funktion kann es sofort zurückgeben, sodass der aufrufende Code Aufgaben mit einem Ergebnis verbinden kann, das noch nicht vorliegt.

Zustände und Abschluss

Ein Promise ist ausstehend, bis es mit einem Wert erfüllt oder mit einem Grund abgelehnt wird. Erfüllte und abgelehnte Promises sind abgeschlossen; ihr Ergebnis kann sich nicht mehr ändern.

„Aufgelöst“ hat eine genauere Bedeutung als das umgangssprachliche Synonym für „erfolgreich“. Ein mit einem anderen ausstehenden Promise aufgelöstes Promise übernimmt dessen späteres Ergebnis, auch eine Ablehnung. Es kann daher aufgelöst und zugleich noch ausstehend sein.

Ein Promise um eine Operation erstellen
function wait(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}
wait(200).then(() => console.log("Pause finished"));

new Promise ruft seine Executor-Funktion sofort auf. Diese registriert einen Timer. Wenn der Timer resolve aufruft, wird das Promise mit undefined erfüllt und der registrierte then-Handler läuft später als Microtask. Diese Hilfsfunktion bietet eine wiederverwendbare Pause, ohne den Thread zu blockieren.

Eine Executor-Funktion kann sowohl resolve als auch reject erhalten:

function reserve(quantity) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (quantity <= 8) resolve(8 - quantity);
      else reject(new Error("Not enough stock"));
    }, 200);
  });
}
reserve(3).then(left => console.log(left));
reserve(9).catch(err => console.log(err.message));

Für diese positiven Mengen wird der erste Aufruf mit 5 erfüllt und der zweite mit „Not enough stock“ abgelehnt. Das Promise lässt den aufrufenden Code beide möglichen Ergebnisse über dasselbe zurückgegebene Objekt erhalten.

Das Erstellen startet die Executor-Funktion
console.log("before");
const result = new Promise(resolve => {
  console.log("creating");
  resolve(12);
});
console.log("after");
result.then(value => console.log(value));

Die Ausgabe lautet „before“, „creating“, „after“, „12“. Das Registrieren von then startet diese Executor-Funktion nicht. Der Konstruktor hat sie bereits ausgeführt; then registriert Code zur Verarbeitung ihres Ergebnisses.

Nur der erste Aufruf von resolve oder reject entscheidet über das weitere Schicksal des Promises. Spätere Aufrufe können diese Entscheidung nicht ersetzen. Das gilt auch, wenn die erste Auflösung ein anderes Promise übernimmt, dessen Ergebnis noch aussteht. Der Aufruf von resolve stoppt den gewöhnlichen Code der Executor-Funktion nicht. Nutze daher return, wenn ein Ausführungszweig enden soll.

Jeden Ausführungspfad korrekt abschließen

Eine synchron in der Executor-Funktion ausgelöste Ausnahme lehnt das Promise automatisch ab. Eine Ausnahme in einem späteren Timer-Callback liegt außerhalb dieses Aufrufs und wird nicht in eine Ablehnung umgewandelt. Nutze dort reject(new Error("...")); andernfalls kann der Fehler unbehandelt bleiben, während das Promise ausstehend bleibt.

Ein Ausführungszweig, der keine der beiden Funktionen aufruft, kann den aufrufenden Code dauerhaft warten lassen. Nutze Error-Objekte als Ablehnungsgründe, damit Handler eine Meldung lesen und Debugging-Werkzeuge nützliche Fehlerinformationen anzeigen können.

Promise.resolve(value) erstellt für einen gewöhnlichen Wert ein erfülltes Promise oder übernimmt das Ergebnis eines anderen Promises. Promise.reject(error) erstellt ein abgelehntes Promise. Registriere bei letzterem eine Behandlung für die Ablehnung.

Ein übernommenes Ergebnis verfolgen
const inner = new Promise(resolve => {
  setTimeout(() => resolve("packed"), 250);
});
const outer = new Promise((resolve, reject) => {
  resolve(inner);
  reject(new Error("Too late to replace it"));
});
outer.then(value => console.log(value));

Die innere Executor-Funktion startet ihren Timer während der Erstellung. Die äußere Executor-Funktion löst dann das äußere Promise mit dem inneren auf und bindet es damit an dessen späteres Ergebnis. Der spätere Aufruf von reject kann diese Entscheidung nicht ersetzen. Das äußere Promise bleibt ausstehend, bis das innere erfüllt wird, und sein Handler gibt nach etwa 250 ms „packed“ aus.

Würde das innere Promise stattdessen abgelehnt, würde das äußere diese Ablehnung übernehmen. Der erste Aufruf von resolve hat ausgewählt, welchem Ergebnis gefolgt wird; er hat keinen Erfolg garantiert. Diese Unterscheidung ist wichtig, wenn ein Wrapper ein anderes Promise weiterreicht, statt einen fertigen gewöhnlichen Wert bereitzustellen.

Wichtige Erkenntnisse

  • Ausstehende Promises werden erfüllt oder abgelehnt; abgeschlossene Ergebnisse bleiben unverändert.
  • Beim Auflösen kann das spätere Ergebnis eines anderen Promises übernommen werden.
  • Die Executor-Funktion läuft während der Erstellung synchron.
  • Nutze resolve und reject, um spätere Callbacks mit dem Promise zu verbinden.
  • Eine später in einem Callback ausgelöste Ausnahme lehnt das umgebende Promise nicht automatisch ab.

3.2 Reagiere mit then, catch und finally auf Promises

Promise-Handler verbinden Code mit einem Erfolg, einem Fehler oder beiden möglichen Ergebnissen. Jede Registrierung eines Handlers gibt außerdem ein neues Promise zurück, sodass der nächste Code auf dem aufbauen kann, was der Handler liefert.

Den passenden Handler für das Ergebnis nutzen

p.then(onFulfilled) erhält den Wert eines erfüllten Promises. p.catch(onRejected) erhält einen Ablehnungsgrund; es ist eine Kurzschreibweise für p.then(undefined, onRejected). Handler sind Funktionen und laufen als Microtasks, auch wenn p bereits abgeschlossen ist.

Promise.resolve("draft")
  .then(text => console.log(text));
console.log("registered");

Das Skript gibt „registered“ aus, bevor der Handler „draft“ ausgibt. Ein sofort verfügbarer Wert macht den Handler nicht synchron.

Es ist entscheidend, eine Funktion zu übergeben. then(console.log("draft")) gibt „draft“ während der Registrierung aus und übergibt undefined als Handler. Nutze then(() => console.log("draft")), wenn die Meldung später ausgegeben werden soll. then(console.log) übergibt die Ausgabefunktion selbst, sodass sie den Wert erhält.

Nach Erfolg oder Fehler aufräumen

finally eignet sich dazu, nach Abschluss einer Operation eine Ladeanzeige auszublenden oder eine Schaltfläche zu aktivieren. Sein Callback erhält weder einen Wert noch einen Fehler als Argument.

Promise.resolve(4)
  .finally(() => console.log("Cleanup"))
  .then(value => console.log(value));

Die Ausgabe lautet „Cleanup“, dann „4“. Gibt der Aufräum-Callback einen gewöhnlichen Wert zurück, ersetzt dieser die 4 nicht. Ein zurückgegebenes Promise kann den nächsten Schritt verzögern, bis das Aufräumen beendet ist. Löst der Aufräum-Callback eine Ausnahme aus oder gibt er ein abgelehntes Promise zurück, wird dieser Fehler zum neuen Ergebnis der Kette.

Ein typischer Ablauf auf einer Seite setzt vor dem Start einen Ladezustand, verarbeitet ein Ergebnis in then, meldet einen Fehler in catch und setzt den Ladezustand in finally zurück. Platziere Arbeit, die vom Ergebnis abhängt, in then oder catch, wo das benötigte Argument vorliegt.

Getrennte Abonnements und eine Kette unterscheiden
const p = Promise.resolve(4);
p.then(value => value * 2);
p.then(value => console.log(value));

Dies sind zwei getrennte Abonnements von p. Beide erhalten 4. Das Ergebnis der Multiplikation gehört zum neuen Promise, das das erste then zurückgibt; es verändert weder p noch liefert es einen Wert an das zweite Abonnement.

Verbinde sie, um den veränderten Wert weiterzugeben:

p.then(value => value * 2)
  .then(value => console.log(value));

Nun erhält der letzte Handler 8. Der Unterschied liegt darin, an welchem Promise der jeweilige Handler registriert ist.

Das zurückgegebene Objekt ist weiterhin ein Promise

In const next = p.then(value => value * 2) ist next ein neues Promise und nicht die Zahl 8. Der Handler kann erst nach dem Ende des umgebenden Skripts laufen und selbst eine verzögerte Aufgabe zurückgeben. Nutze einen weiteren Handler oder await, um den Wert zu erhalten. Wenn du next sofort wie eine Zahl behandelst, geht diese zeitliche Information verloren.

Wichtige Erkenntnisse

  • Nutze then für Werte, catch für Ablehnungen und finally zum Aufräumen.
  • Übergib Funktionen als Handler.
  • Jedes then gibt ein neues Promise zurück.
  • Getrennte Handler an einem Promise erhalten dessen ursprüngliches Ergebnis.
  • Erfolgreiches Aufräumen in finally bewahrt das vorherige Ergebnis; schlägt es fehl, ersetzt der Fehler dieses Ergebnis.

3.3 Verkette Promises zu einer Abfolge

Eine Promise-Kette macht die Ausgabe eines Handlers zur Eingabe des nächsten. Jedes Glied muss den Wert oder das Promise zurückgeben, das die späteren Glieder benötigen.

Werte durch die Kette weitergeben
Promise.resolve(3)
  .then(count => count * 4)
  .then(total => total + 2)
  .then(total => console.log(total));

Der erste Handler erhält 3 und gibt 12 zurück. Der zweite erhält 12 und gibt 14 zurück. Der letzte Handler gibt „14“ aus. Ein Ausdrucksrumpf einer Pfeilfunktion gibt seinen Wert automatisch zurück; ein Funktionsrumpf mit geschweiften Klammern benötigt ein ausdrückliches return, um einen Wert weiterzugeben.

Ein Promise für einen abhängigen Schritt zurückgeben
function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
function makeCode() {
  return wait(100).then(() => "P4");
}
function makeLabel(code) {
  return wait(200).then(() => "Parcel " + code);
}
makeCode()
  .then(code => makeLabel(code))
  .then(label => console.log(label));

makeCode liefert nach etwa 100 ms „P4“. Der nächste Handler gibt das Promise von makeLabel zurück. Die Kette folgt diesem Promise, wartet etwa weitere 200 ms und gibt „Parcel P4“ an den letzten Handler weiter. Der aufrufende Code erhält eine Beschriftung und kein innerhalb des Werts verschachteltes Promise.

Ein unterbrochenes Glied finden

Vergleiche den mittleren Handler mit diesem:

.then(code => {
  makeLabel(code);
})

Er startet makeLabel, gibt aber nichts zurück. Sein neues Promise wird mit undefined erfüllt, ohne auf die Beschriftung zu warten. Der nächste Handler erhält daher nach dem Schritt für den Code undefined, während die Erstellung der Beschriftung getrennt weiterläuft. Auch Fehler aus dieser getrennten Arbeit werden nicht mehr durch die äußere Kette weitergegeben.

Korrigiere dies mit return makeLabel(code); innerhalb der geschweiften Klammern oder entferne die Klammern, um den Ausdruck zurückzugeben. Dieselbe Regel gilt, wenn ein Handler eine Pause startet: .then(() => wait(200)) wartet; .then(() => { wait(200); }) wartet nicht.

Verschachtelung verringern und benötigte Werte behalten

Ein weiteres then tief innerhalb des ersten Handlers kann erneut eine Verschachtelung wie bei Callbacks erzeugen. Gib das innere Promise zurück und platziere den nächsten Handler auf der äußeren Ebene. So bleiben Zeitablauf, Werte und Fehler auf einem gemeinsamen Pfad.

Jeder Handler hat seinen eigenen lokalen Gültigkeitsbereich. Benötigt ein späterer Schritt sowohl den Code als auch die Beschriftung, gib ein Objekt zurück, das beide enthält:

makeCode()
  .then(code => makeLabel(code).then(label => ({ code, label })))
  .then(pair => console.log(pair.code + ": " + pair.label));

Dieser kleine innere Handler erstellt den kombinierten Wert und wird zurückgegeben, sodass er mit der Kette verbunden bleibt. Eine async-Funktion kann beide Ergebnisse ebenfalls in gewöhnlichen lokalen Variablen speichern.

Eine Ablehnung überspringt spätere Erfüllungshandler, bis ein Ablehnungshandler erreicht wird. Ein abschließendes catch kann Fehler aus allen vorherigen verbundenen Schritten behandeln. Abhängige Arbeit gehört in diese Abfolge; unabhängige Arbeit kann mit Promise.all gemeinsam gestartet werden.

Wichtige Erkenntnisse

  • Gib den Wert zurück, den der nächste Handler benötigt.
  • Gibst du ein Promise zurück, wartet die Kette auf dessen Ergebnis.
  • Pfeilfunktionen mit Blockrumpf benötigen return.
  • Halte abhängige Arbeit und ihre Fehler mit der äußeren Kette verbunden.
  • Gib kombinierte Daten weiter, wenn spätere Schritte frühere lokale Werte benötigen.
4

Schreibe asynchronen Code mit async und await

Mit async-Funktionen kannst du Schritte mit Promises in der Reihenfolge schreiben, in der sie ablaufen sollen. Werte können in benannten Variablen bleiben und await markiert die Stellen, an denen die Funktion anhält. Lerne, wie sich diese Pause auf den aufrufenden Code auswirkt, wandle älteren Code in diesen Stil um und erkenne Fehler, durch die ein Promise dort steht, wo ein Wert benötigt wird. Achte besonders auf Schleifen: Ein asynchroner Callback bringt seinen Aufrufer nicht dazu zu warten.

4.1 Schreibe async-Funktionen und nutze await

Eine async-Funktion gibt immer ein Promise zurück. Innerhalb der Funktion hält await sie an, bis ein Wert verfügbar ist, sodass sich abhängige Schritte von oben nach unten lesen lassen.

Einen Wert über ein Promise zurückgeben
async function getCount() {
  return 6;
}
getCount().then(count => console.log(count));

Der Aufruf von getCount() liefert ein Promise, das mit 6 erfüllt wird. Der Handler gibt „6“ aus. Löst die Funktion eine Ausnahme aus, wird das zurückgegebene Promise abgelehnt. Gibt sie ein anderes Promise zurück, übernimmt die async-Funktion dessen späteres Ergebnis.

Du kannst eine async-Funktion mit async function name(), als asynchrone Pfeilfunktion wie const name = async () => {} oder als asynchrone Objektmethode deklarieren. Das Verhalten bei der Rückgabe des Promises ist gleich.

Bei await anhalten
function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
async function showSteps() {
  console.log("Step one");
  await wait(300);
  console.log("Step two");
}
showSteps();
console.log("Caller continues");

Die Ausgabe beginnt mit „Step one“, dann folgt „Caller continues“. Etwa 300 ms später wird „Step two“ ausgegeben. Beim Aufruf der Funktion läuft ihr Rumpf synchron bis zum ersten await. Die Pause gibt die Kontrolle an den aufrufenden Code zurück. Sobald das Promise erfüllt ist, wird der Rest von showSteps über die Microtask-Warteschlange fortgesetzt.

Await hält seine Funktion an, nicht den ganzen Thread. Andere bereite Tasks können während des Wartens laufen. Eine große synchrone Berechnung vor oder nach dem await kann den Thread während ihrer Ausführung weiterhin blockieren.

Einen Wert lesen oder einen Fehler erhalten

Ein erfülltes Promise liefert seinen Wert: const count = await getCount(); setzt count auf 6. Ein abgelehntes Promise löst seinen Grund an der await-Zeile aus. Umgib die erwartete Operation mit try und catch, wenn diese Funktion den Fehler behandeln soll.

await akzeptiert auch gewöhnliche Werte. In const count = await 6; ist das Ergebnis weiterhin 6, aber die Fortsetzung der Funktion wird auf später verschoben. Sie wird nicht synchron, nur weil keine wartende Operation nötig war.

Prüfen, wohin await gehört

Nutze await innerhalb der async-Funktion, deren Rumpf es enthält. Es ist auch auf der obersten Ebene eines JavaScript-Moduls erlaubt. Ein gewöhnlicher Funktionsrumpf darf kein await enthalten; eine äußere async-Funktion macht einen verschachtelten gewöhnlichen Callback nicht asynchron.

Der aufrufende Code erhält weiterhin ein Promise. Um den endgültigen Wert zu nutzen, warte aus einer anderen async-Funktion darauf oder registriere then. Zum Beispiel speichert const total = getCount(); ein Promise und nicht 6.

Ein Event-Handler kann selbst asynchron sein. Der Handler kann auf einen Vorbereitungsschritt warten, bevor er eine Seite aktualisiert. Gib dieser Aktion eine eigene Fehlerbehandlung, denn die Ereignisauslösung wartet nicht auf das vom Handler zurückgegebene Promise. Auf der obersten Ebene eines Programms gibt ein Aufruf wie showSteps().catch(console.error) einem unerwarteten Fehler einen Handler.

Wichtige Erkenntnisse

  • Eine async-Funktion gibt ein Promise für ihren Rückgabewert oder ausgelösten Fehler zurück.
  • Code vor dem ersten await läuft während des Aufrufs.
  • Nach await wird die Funktion später mit einem Wert fortgesetzt oder der Ablehnungsgrund wird ausgelöst.
  • Await gehört in eine async-Funktion oder auf die oberste Modulebene.

4.2 Schreibe Promise-Ketten und die Nutzung von Callback-APIs mit async und await um

Eine Promise-Kette kann zu einer async-Funktion werden, ohne ihre Abhängigkeiten zu verändern. Jedes Ergebnis erhält eine lokale Variable und jeder mit await erwartete Schritt beginnt erst, wenn das benötigte frühere Ergebnis vorliegt.

Handler durch Zuweisungen mit await ersetzen
function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
function getUser(id) {
  return wait(100).then(() => ({ id, name: "Nia" }));
}
function getOrders(user) {
  return wait(200).then(() => [user.name + "-1", user.name + "-2"]);
}
async function showOrders(id) {
  const user = await getUser(id);
  const orders = await getOrders(user);
  console.log(user.name + ": " + orders.length + " orders");
}
showOrders(8).catch(console.error);

Das erste await liefert einen Nutzer. Erst dann kann getOrders(user) starten. Nach insgesamt etwa 300 ms gibt die Funktion „Nia: 2 orders“ aus. Sowohl user als auch orders liegen für diese letzte Zeile im Gültigkeitsbereich. In einer Kette würde der Parameter jedes Handlers zu einem getrennten lokalen Gültigkeitsbereich gehören.

Die entsprechende Kette würde getOrders(user) aus dem ersten Handler zurückgeben. Das Umschreiben mit await macht dieses Warten sichtbar und erhält zugleich die Reihenfolge. Ein abschließendes catch der Kette wird zu einem try/catch um die erwarteten Schritte; finally wird zu einem Aufräumblock.

Eine Callback-API in eine Promise-API umwandeln

Wenn du einem Wrapper async hinzufügst, wird der verzögerte Wert einer Callback-API nicht zu dessen Rückgabewert. Verbinde den Callback mit dem Abschluss des Promises:

function readTag(id, callback) {
  setTimeout(() => {
    if (id <= 0) callback(new Error("Invalid tag"));
    else callback(null, "Tag " + id);
  }, 100);
}
function readTagAsync(id) {
  return new Promise((resolve, reject) => {
    readTag(id, (err, tag) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(tag);
    });
  });
}

Innerhalb einer async-Funktion liefert const tag = await readTagAsync(8); „Tag 8“. Ein Aufruf mit 0 löst stattdessen bei await „Invalid tag“ aus. Bei Erfolg wird das Promise des Wrappers aufgelöst, bei einem Fehler abgelehnt. Der Adapter ändert, wie das Ergebnis bereitgestellt wird, nicht was die zugrunde liegende Operation tut.

Diese Umwandlung heißt Promisifizierung. Für gewöhnliche Error-first-APIs mit einem Ergebnis stellt Node.js außerdem util.promisify bereit, und viele eingebaute Module haben bereits Promise-basierte Varianten wie fs/promises.

Einen klaren Pfad beibehalten

Mit einer Hilfsfunktion wait, die ein Promise zurückgibt, werden Timer zu Pausen. Eine asynchrone Schleife kann einen Schritt ausgeben, auf die Pause warten und dann den nächsten Schleifendurchlauf beginnen. Mit Callbacks müsste der nächste Schritt innerhalb des vorherigen Callbacks eingeplant werden.

Promises, Ketten und async-Funktionen arbeiten zusammen: Eine async-Funktion kann auf eine Kette warten und der aufrufende Code kann then am zurückgegebenen Promise einer async-Funktion registrieren. Nutze innerhalb einer Funktion die Form, mit der Abhängigkeiten und Fehlerbehandlung leicht nachzuvollziehen sind. Ein zusätzlicher Promise-Konstruktor ist nützlich, um einen Callback anzupassen, aber auf vorhandene Operationen, die Promises zurückgeben, kann direkt mit await gewartet werden.

Wichtige Erkenntnisse

  • Jeder abhängige Schritt einer Kette kann zu einer Zuweisung mit await werden.
  • Lokale Variablen halten frühere Ergebnisse später in der async-Funktion verfügbar.
  • Promisifiziere Callbacks, indem du Fehler an reject und Ergebnisse an resolve weitergibst.
  • Auf vorhandene Promise-APIs kann direkt mit await gewartet werden.

4.3 Vermeide häufige Fehler mit async und await

Asynchroner Code kann synchron aussehen und dennoch Promises zwischen Funktionen weitergeben. Viele Fehler entstehen, weil der spätere Wert zu früh gelesen wird oder weil man annimmt, eine Hilfsfunktion warte auf Callbacks, die sie ignoriert.

Vor der Verwendung auf den Wert warten
async function getQuantity() {
  return 18;
}
async function total() {
  const quantity = await getQuantity();
  console.log(quantity * 3);
}
total().catch(console.error);

Dies gibt „54“ aus. Ohne await würde quantity ein Promise enthalten. Die Multiplikation dieses Promises mit 3 ergibt NaN und keine verzögerte Berechnung, die sich automatisch aktualisiert.

Eine Bedingung enthält dieselbe Falle. Eine asynchrone Prüfung, die false zurückgibt, gibt dennoch ein Promise-Objekt zurück, und Objekte sind truthy. Nutze if (await allowed()) innerhalb einer async-Funktion, wenn der Ausführungszweig vom späteren booleschen Wert abhängt. if (allowed()) prüft das Objekt selbst.

forEach sammelt keine Callback-Promises
function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
async function labelAll(ids) {
  ids.forEach(async id => {
    await wait(100);
    console.log("Label " + id);
  });
  console.log("Labels finished");
}
labelAll([4, 9]).catch(console.error);

„Labels finished“ erscheint vor den Beschriftungen. forEach ruft beide Callbacks auf, ignoriert deren zurückgegebene Promises und beendet seine eigene synchrone Iteration. Selbst await ids.forEach(...) würde auf undefined warten, nicht auf die Arbeit der Callbacks. Ein Fehler innerhalb dieser ignorierten Promises liegt ebenfalls außerhalb des von labelAll zurückgegebenen Promises.

Schreibe für eine Verarbeitung in Reihenfolge eine gewöhnliche Schleife:

for (const id of ids) {
  await labelOne(id);
}
console.log("Labels finished");

Hier ist labelOne eine Operation, die ein Promise zurückgibt. Jeder Eintrag beginnt, nachdem der vorherige abgeschlossen ist. Verwende für unabhängige Einträge vor der letzten Ausgabe await Promise.all(ids.map(id => labelOne(id)));. map sammelt die zurückgegebenen Promises und Promise.all gibt dem aufrufenden Code ein Promise für deren Abschluss.

Unabhängigkeit und Syntax auseinanderhalten

Wartest du sofort auf jede Operation, laufen die Wartezeiten nacheinander ab. Für unabhängige Wartezeiten von 1000, 2000 und 1500 ms dauert das etwa 4500 ms. Startest du alle drei und kombinierst sie, dauert es etwa 2000 ms. Ein Schritt, der ein früheres Ergebnis benötigt, muss weiterhin darauf warten.

Fehler verbunden halten

Eine verschachtelte Funktion benötigt ihr eigenes Schlüsselwort async, um await zu nutzen. Dieses Schlüsselwort allein bringt den aufrufenden Code aber nicht dazu, auf das Ergebnis zu warten. Prüfe beide Seiten: Gibt der Callback ein Promise zurück und nutzt der aufrufende Code es?

Für Arbeit, die absichtlich gestartet wird, ohne auf ihren Abschluss zu warten, registriere einen Ablehnungshandler wie backgroundTask().catch(console.error). Prüfe auch Rückgaben in async-Wrappern: async function prepare() { await wait(100); } wird mit undefined erfüllt. Auf einen Schritt zu warten und sein Ergebnis zurückzugeben sind getrennte Entscheidungen.

Ergebnisse der korrigierten Gruppe zurückgeben
function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
async function prepareItem(id) {
  await wait(50);
  return "Item " + id;
}
async function prepareAll(ids) {
  return await Promise.all(ids.map(id => prepareItem(id)));
}
prepareAll([6, 2]).then(items => console.log(items.join(", ")));

Jeder map-Callback gibt ein Promise zurück und map speichert diese Promises in einem Array. Promise.all wartet auf beide und liefert ["Item 6", "Item 2"]. Die Funktion prepareAll gibt dieses Array über ihr eigenes Promise zurück, sodass der aufrufende Code nach Abschluss der Gruppe „Item 6, Item 2“ ausgibt.

Würdest du die Rückgabe von prepareAll entfernen, würde die Funktion weiterhin auf die Gruppe warten, aber der aufrufende Code erhielte undefined. Würdest du map durch forEach ersetzen, ginge das Array der Promises verloren. Prüfe beim Korrigieren einer Aktion mit mehreren Einträgen das Sammeln, Warten und Zurückgeben getrennt.

Wichtige Erkenntnisse

  • Warte mit await, bevor du eine spätere Zahl oder einen späteren booleschen Wert verwendest.
  • forEach ignoriert Promises, die asynchrone Callbacks zurückgeben.
  • Nutze for...of für sequenzielle Arbeit oder map mit Promise.all für unabhängige Arbeit.
  • Ein verschachtelter Callback benötigt sein eigenes Schlüsselwort async.
  • Verbinde absichtlich nicht erwartete Arbeit mit einem Ablehnungshandler.
5

Behandle Fehler in asynchronem Code

Eine fehlgeschlagene Operation ist Teil des Kontrollflusses eines Programms. Der Code muss entscheiden, ob er die Verarbeitung fortsetzt, einen Wiederholungsversuch startet, den Fehler meldet oder ihn an einen Aufrufer weitergibt, der entscheiden kann. Verfolge Fehler durch Promise-Ketten und mit await erwartete Aufrufe und nutze anschließend Aufräumcode, der bei beiden Ergebnissen läuft. Platziere die Fehlerbehandlung schließlich an der Grenze einer Aktion, damit der Nutzer eine hilfreiche Meldung erhält und du die technische Ursache behältst.

5.1 Behandle Ablehnungen in Promise-Ketten

Die Ablehnung eines Promises folgt der verbundenen Kette, bis ein Ablehnungshandler sie behandelt. Wenn du verstehst, was dieser Handler zurückgibt, weißt du, ob die Kette nach dem Fehler fortgesetzt wird oder weiterhin fehlschlägt.

Eine Ablehnung an Erfolgshandlern vorbei verfolgen
Promise.resolve(8)
  .then(value => {
    throw new Error("Count rejected");
  })
  .then(value => console.log("Unused: " + value))
  .catch(err => {
    console.log(err.message);
    return 2;
  })
  .then(value => console.log("Using " + value));

Die Ausnahme lehnt das neue Promise ab, das das erste then zurückgibt. Der nächste Erfüllungshandler wird übersprungen, sodass „Unused“ nie ausgegeben wird. Das catch gibt „Count rejected“ aus und liefert 2 zurück. Diese Rückgabe erfüllt das nächste Glied, sodass der letzte Handler „Using 2“ ausgibt.

Ein ausdrücklich abgelehntes Promise und eine innerhalb eines Handlers ausgelöste Ausnahme gelangen auf denselben Ablehnungspfad. Deshalb kann ein abschließendes catch Fehler aus mehreren vorherigen verbundenen Schritten behandeln.

Zwischen Fortsetzung und Weitergabe wählen

Ein Ablehnungshandler, der einen Wert zurückgibt, setzt die Verarbeitung nach dem Fehler mit diesem Wert fort. Gibt er nichts zurück, wird die Verarbeitung ebenfalls fortgesetzt, aber mit undefined. Einen Fehler zu protokollieren ist daher nicht dasselbe, wie die Kette abgelehnt zu lassen:

Promise.reject(new Error("Label missing"))
  .catch(err => console.log(err.message))
  .then(value => console.log(value));

Der letzte Handler erhält undefined. Benötigt späterer Code eine tatsächliche Beschriftung, ist diese stille Fortsetzung wahrscheinlich falsch. Gib einen nutzbaren Ersatzwert zurück oder löse den Fehler erneut aus.

function reportAndRethrow(err) {
  console.error(err);
  throw err;
}

Wird diese Funktion in catch verwendet, protokolliert sie den Fehler und das nächste Promise bleibt mit demselben Grund abgelehnt. Ein späterer Aufrufer in der Kette kann entscheiden, was zu tun ist.

Den Handler nach der geschützten Arbeit platzieren

Ein catch behandelt Fehler oberhalb seiner Position. Setzt es die Verarbeitung nach einem Fehler fort, laufen spätere Schritte normal weiter und deren neue Fehler benötigen einen späteren Handler. Ein Handler in der Mitte kann keine Fehler abfangen, die noch nicht aufgetreten sind.

Es gibt außerdem einen wichtigen Unterschied zwischen .then(success, failure) und .then(success).catch(failure). Die beiden Handler in einem then reagieren auf das Ergebnis des eingehenden Promises. Sein Fehlerhandler fängt keine Ausnahme ab, die sein eigener Erfolgshandler auslöst. Dieser Fehler gehört zum neuen Promise, das der Aufruf zurückgibt, sodass ein nachfolgendes catch benötigt wird.

Nützliche Fehler erhalten

Lehne mit Error-Objekten ab, damit Handler err.message und err.name lesen können. Eine Zeichenkette auszulösen oder als Ablehnungsgrund zu verwenden ist erlaubt, aber Code, der die Felder eines Error-Objekts erwartet, erhält sie nicht. Bewahre beim Weitergeben eines Fehlers den ursprünglichen Fehler, sofern zusätzlicher Kontext dem Aufrufer nicht hilft zu verstehen, welche Operation fehlgeschlagen ist.

Wichtige Erkenntnisse

  • Ablehnungen überspringen Erfüllungshandler, bis ein Ablehnungshandler erreicht wird.
  • Eine Rückgabe aus catch setzt die Verarbeitung fort; ohne Rückgabewert geschieht das mit undefined.
  • Löse einen Fehler erneut aus, damit er weitergegeben wird.
  • Ein catch schützt die verbundenen Schritte davor.
  • Ein Ablehnungshandler von then kann eine Ausnahme seines zugehörigen Erfolgshandlers nicht abfangen.

5.2 Nutze try, catch und finally mit await

try, catch und finally ermöglichen einer async-Funktion, Fehler dort zu behandeln, wo sie auf Ergebnisse wartet. Nutze diese Struktur, um Ergebnisverarbeitung, Fortsetzung nach Fehlern und Aufräumen zusammenzuhalten.

Ein Fehler überträgt die Kontrolle

Ein Error-Objekt beschreibt einen Fehler. new Error("Label missing") hat die Meldung „Label missing“ und den Namen „Error“; Debugging-Werkzeuge können auch seinen Stacktrace anzeigen. Eingebaute Fehler haben Typen wie TypeError.

throw stoppt an dieser Stelle den normalen Ablauf und überträgt die Kontrolle an das nächstgelegene passende catch. Code nach der Behandlung kann weiterhin laufen, wenn die Funktion ihren eigenen Fehler abfängt. Löse Error-Objekte aus, damit spätere Handler nützliche Felder erhalten.

Innerhalb des geschützten Blocks warten
async function run() {
  console.log("Working");
  try {
    await Promise.reject(new Error("Device busy"));
    console.log("Complete");
  } catch (err) {
    console.log("Failed: " + err.message);
  } finally {
    console.log("Controls enabled");
  }
}
run().catch(console.error);

Das abgelehnte Promise löst an der await-Zeile eine Ausnahme aus. Der Rest von try wird übersprungen. Die Ausgabe lautet „Working“, „Failed: Device busy“, dann „Controls enabled“. Eine erfolgreiche Operation würde „Complete“ erreichen und trotzdem denselben Aufräumcode ausführen.

Nutze finally für eine Ladeanzeige, den Zustand einer Schaltfläche oder eine andere Ressource, die nach beiden möglichen Ergebnissen zurückgesetzt werden muss. Der Block läuft auch, wenn try oder catch eine return-Anweisung enthält. Vermeide, dass der Aufräumcode eine Ausnahme auslöst, sofern dieser neue Fehler nicht absichtlich das frühere Ergebnis ersetzen soll. Ein return in finally kann eine frühere Rückgabe oder Ausnahme überschreiben. Vermeide es daher beim gewöhnlichen Aufräumen.

Ein Promise zurückzugeben ist hier etwas anderes als darauf zu warten
async function readSetting() {
  try {
    return await Promise.reject(new Error("Missing setting"));
  } catch {
    return "light";
  }
}
readSetting().then(value => console.log(value));

Dies gibt „light“ aus. Die Funktion wartet innerhalb von try, sodass die Ablehnung das lokale catch erreicht. Entfernst du await, wird das abgelehnte Promise sofort zurückgegeben; die Funktion übernimmt seine Ablehnung, aber das lokale catch behandelt dieses spätere Ergebnis nicht. Der aufrufende Code müsste es abfangen.

Für jeden Fehler einen passenden Fehlerbehandlungsbereich wählen

Ein try kann eine Gruppe von awaits abdecken, wenn die gesamte Aktion bei einem Fehler in einem beliebigen Schritt stoppen soll. Gib einem optionalen Schritt einen kleineren Fehlerbehandlungsbereich, wenn er unabhängig fortgesetzt werden kann. Zum Beispiel nutzt const settings = await loadSettings().catch(() => ({ theme: "light" })); ein Standardobjekt und fährt fort.

Eine innerhalb von try deklarierte Variable hat Blockgültigkeitsbereich. Benötigt Code nach dem Block sie, deklariere let result; vor try oder erledige die vom Ergebnis abhängige Arbeit innerhalb von try. Verschiebe einen Wert nicht allein deshalb aus seinem Gültigkeitsbereich, um eine abschließende Ausgabe an einer bevorzugten Stelle zu behalten.

Kann die aktuelle Funktion die Verarbeitung nicht fortsetzen, lass die Ablehnung den aufrufenden Code erreichen. Fängst du sie nur ab, um sie zu verbergen, kann der aufrufende Code keine sinnvolle Entscheidung treffen.

Wichtige Erkenntnisse

  • Erwartete Ablehnungen lösen an der await-Zeile eine Ausnahme aus.
  • Catch kann die Verarbeitung fortsetzen, einen Fehler melden oder ihn erneut auslösen.
  • Finally läuft nach beiden möglichen Ergebnissen, auch bei einer return-Anweisung.
  • Nutze return await innerhalb von try, wenn das lokale catch die Ablehnung der zurückgegebenen Operation behandeln muss.
  • Ergebnisse mit Blockgültigkeitsbereich bleiben innerhalb ihres try-Blocks.

5.3 Mache Fehler sichtbar und setze die Verarbeitung nach Möglichkeit sinnvoll fort

Eine Aktion braucht einen Fehlerbehandlungsbereich: eine Stelle, die ihren endgültigen Fehler erhält und eine Reaktion auswählt. Ohne diesen Bereich kann ein Promise abgelehnt werden, während niemand sein Ergebnis behandelt.

Den letzten Pfad behandeln

Eine unbehandelte Promise-Ablehnung ist eine Ablehnung, für die bei der Prüfung durch die Hostumgebung noch ein Handler fehlt. Die Behandlung kann mit catch, dem Ablehnungsargument von then oder mit try/catch um await erfolgen. Ein Browser meldet das Problem und kann unhandledrejection auslösen. Node.js beendet das Programm bei unbehandelten Ablehnungen unter seiner normalen Einstellung standardmäßig, sodass eine unbehandelte Ablehnung ein Programm beenden kann.

Eine Aktion wie main() oder ein asynchroner Klick-Handler sollte ihr letztes Promise behandeln. Funktionen auf niedrigerer Ebene können Fehler nach oben weitergeben, aber der Pfad muss an einer Stelle enden, die entscheidet, was zu tun ist. Ein globaler Listener für Ablehnungen kann helfen, übersehene Fehler zu protokollieren; er kann nicht für jede Aktion die beste Fortsetzung wählen.

Ein leeres catch verschluckt den Fehler. Protokolliere technische Einzelheiten mit console.error(err) und zeige eine klare Meldung an, wenn die Aktion des Nutzers fehlgeschlagen ist. Eine Meldung wie „Die Beschriftung konnte nicht gespeichert werden. Versuche es gleich noch einmal.“ zeigt dem Nutzer eine Handlungsmöglichkeit, ohne einen Stacktrace offenzulegen.

Kontext ergänzen, ohne die Ursache zu verlieren
async function prepareLabel() {
  try {
    return await Promise.reject(new Error("Device offline"));
  } catch (err) {
    throw new Error("Could not prepare label", { cause: err });
  }
}
prepareLabel().catch(err => {
  console.log(err.message);
  console.log(err.cause.message);
});

Dies gibt „Could not prepare label“ und „Device offline“ aus. Die äußere Meldung benennt die fehlgeschlagene Aktion. Die Eigenschaft cause bewahrt den Fehler auf niedrigerer Ebene für die Diagnose. Die Error-Option für die Fehlerursache ist Teil von ECMAScript 2022.

Eine neue Operation mit begrenzter Anzahl von Versuchen wiederholen
function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
async function withRetry(task, attempts) {
  for (let attempt = 1; attempt <= attempts; attempt++) {
    try {
      return await task();
    } catch (err) {
      if (attempt === attempts) throw err;
      await wait(200);
    }
  }
}

Rufe diese Funktion mit einer Funktion auf, die einen neuen Versuch startet, und einer positiven Ganzzahl als Obergrenze. Schlägt task zweimal fehl und ist beim dritten Aufruf erfolgreich, stoppt die Funktion bei einer Grenze von vier beim dritten Aufruf und gibt dessen Wert zurück. Schlagen alle vier Versuche fehl, wird der vierte Fehler erneut ausgelöst. Die Verzögerung tritt nur zwischen den Versuchen auf.

return await hält eine Ablehnung innerhalb dieses try, wo catch einen Wiederholungsversuch starten kann. Würdest du statt einer Aufgabenfunktion ein bereits abgelehntes Promise übergeben, würde die Arbeit nicht erneut gestartet.

Wiederhole eine Operation, wenn ein Fehler vorübergehend sein kann. Nutze eine Rückfalllösung, wenn die Funktion mit Standarddaten arbeiten kann. Brich ab und informiere den Nutzer, wenn die Aktion nicht gelingen kann. Kann eine Funktion auf niedrigerer Ebene diese Entscheidung nicht treffen, bewahre den Fehler und lass den aufrufenden Code entscheiden.

Wichtige Erkenntnisse

  • Jede asynchrone Aktion benötigt einen abschließenden Fehlerhandler.
  • Ein leeres catch verbirgt nützliche Fehlerinformationen.
  • Eine Error-Ursache bewahrt den ursprünglichen Fehler und ergänzt ihn um Kontext.
  • Wiederhole eine Aufgabe durch erneuten Aufruf und begrenze die Anzahl der Versuche.
  • Wähle je nach Fehler einen Wiederholungsversuch, eine Rückfalllösung, einen Abbruch oder die Weitergabe.
6

Führe asynchrone Aufgaben parallel aus

Manche Operationen benötigen frühere Ergebnisse; andere können gemeinsam beginnen. Erkenne diese Abhängigkeit, bevor du entscheidest, wo await stehen soll. Du kombinierst unabhängige Operationen, hältst Ergebnisse in der richtigen Reihenfolge und entscheidest, ob dein Programm jeden Erfolg, jedes Ergebnis, den ersten Abschluss oder den ersten Erfolg benötigt. Jede Wahl verändert, was ein Fehler bedeutet und wie lange der aufrufende Code wartet.

6.1 Starte Aufgaben gemeinsam und warte mit Promise.all auf ihre Ergebnisse

Unabhängige asynchrone Operationen können gleichzeitig warten. Ob du sie gemeinsam startest, hängt davon ab, welche Daten sie benötigen, nicht davon, ob du das Wort async hinzufügst.

Unabhängige Operationen starten, bevor auf alle gewartet wird
function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
function makePart(name, ms) {
  return wait(ms).then(() => name);
}
async function assemble() {
  const parts = await Promise.all([
    makePart("cover", 1000),
    makePart("pages", 2000),
    makePart("label", 1500)
  ]);
  console.log(parts.join(", "));
}
assemble().catch(console.error);

Die drei Aufrufe starten, bevor Promise.all wartet. Etwa 2000 ms später enthält das Array ["cover", "pages", "label"]. Würdest du auf jeden Aufruf warten, bevor du den nächsten startest, dauerte es stattdessen etwa 4500 ms. Timerverzögerungen sind Näherungswerte, daher können die tatsächlich verstrichenen Zeiten länger sein.

Hier bedeutet parallel, dass sich Wartezeiten überschneiden. Die Promise-Handler laufen weiterhin einzeln auf dem Hauptthread. Würdest du drei aufwendige synchrone Funktionen innerhalb des Arrays aufrufen, liefen diese Berechnungen nacheinander; Promise.all würde sie nicht auf getrennte Threads verschieben.

Ergebnisse behalten ihre Eingabepositionen

Die Beschriftung wird vor den Seiten fertig, bleibt aber an dritter Stelle im Array. Promise.all behält die Eingabereihenfolge bei, nicht die Abschlussreihenfolge. Mit Destrukturierung kannst du diese Positionen benennen:

const [cover, pages] = await Promise.all([
  makePart("cover", 1000),
  makePart("pages", 2000)
]);

Nutze diesen Code innerhalb einer async-Funktion. Bei einer Sammlung startet ids.map(id => loadItem(id)) eine Operation pro Kennung, die ein Promise zurückgibt, und übergibt Promise.all das Promise-Array. Warte nicht innerhalb einer Schleife auf jeden Eintrag, wenn alle unabhängig sind und sich überschneiden sollen.

Ein Fehler beendet das gemeinsame Warten

Promise.all wird abgelehnt, sobald eine Eingabe abgelehnt wird. Schlägt ein Teil nach etwa 300 ms fehl, kann ein catch den Grund dann erhalten; es muss nicht auf den am längsten dauernden erfolgreichen Teil warten.

Die anderen Operationen laufen weiter. Ihre Seiteneffekte können weiterhin auftreten, aber das kombinierte Promise wird später nicht mit ihren Werten erfüllt. Dieses Fail-fast-Verhalten ist nützlich, wenn der nächste Schritt jedes Ergebnis benötigt.

Abhängigkeiten beachten und Handler rechtzeitig registrieren

Benötigt eine Operation einen Nutzer, den eine andere erzeugt, ermittle zuerst den Nutzer. Die späteren Aufrufe können nur kombiniert werden, wenn sie voneinander unabhängig sind, sobald der Nutzer vorliegt.

Startest du zwei Promises und wartest getrennt auf sie, überschneiden sich ihre Wartezeiten zwar, aber das zweite kann abgelehnt werden, bevor der Code sein await erreicht. Hat es bei der Prüfung durch die Hostumgebung noch keinen Handler, kann eine unbehandelte Promise-Ablehnung gemeldet werden. Kombinierst du die Promises rechtzeitig mit Promise.all, wird die Behandlung an beiden Eingaben registriert und der aufrufende Code erhält einen Fehlerpfad.

Gewöhnliche Werte sind unter den Eingaben erlaubt und behalten ihre Positionen. Promise.all([]) ist bereits mit einem leeren Array erfüllt. Ein daran registrierter Handler läuft dennoch asynchron.

Wichtige Erkenntnisse

  • Starte unabhängige Operationen gemeinsam, damit sich ihre Wartezeiten überschneiden.
  • Promise.all gibt Ergebnisse in Eingabereihenfolge zurück.
  • Eine Ablehnung lehnt das kombinierte Promise ab, ohne andere Eingaben abzubrechen.
  • Abhängige Schritte benötigen weiterhin ihre früheren Ergebnisse.
  • Kombiniere gestartete Promises rechtzeitig, damit ihre Fehlerpfade verbunden bleiben.

6.2 Wähle zwischen Promise.allSettled, Promise.race und Promise.any

Der richtige Promise-Kombinator hängt davon ab, was als nützliches Ergebnis zählt. Entscheide, ob du alle Erfolge, alle Ergebnisse, den ersten Abschluss oder den ersten Erfolg benötigst.

Teilergebnisse mit allSettled behalten

Promise.allSettled wartet auf jede Eingabe und gibt Ergebnisdatensätze in Eingabereihenfolge zurück. Ein Erfolg wird zu { status: "fulfilled", value }; ein Fehler wird zu { status: "rejected", reason }. Eine abgelehnte Eingabe lehnt das kombinierte Promise nicht ab.

Promise.allSettled([
  Promise.resolve("Map ready"),
  Promise.reject(new Error("Chart unavailable"))
]).then(results => {
  for (const result of results) {
    if (result.status === "fulfilled") console.log(result.value);
    else console.log(result.reason.message);
  }
});

Dies gibt „Map ready“ und „Chart unavailable“ aus. Es eignet sich für unabhängige Bereiche, bei denen ein funktionierender Bereich auch dann nützlich bleibt, wenn ein anderer fehlschlägt. Lies value nur bei einem erfüllten Datensatz und reason bei einem abgelehnten Datensatz.

Mit race den ersten Abschluss entscheiden lassen

Promise.race folgt der ersten Eingabe, die abgeschlossen wird, unabhängig davon, ob sie erfüllt oder abgelehnt wird. Ein Fehler nach 100 ms gewinnt vor einem Erfolg nach 200 ms. Das ist für eine Zeitbegrenzung nützlich:

function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error("Time limit reached")), ms);
  });
}
Promise.race([
  wait(600).then(() => "Prepared"),
  timeout(250)
]).then(console.log).catch(err => console.log(err.message));

Der Timeout wird zuerst abgelehnt, daher lautet die Ausgabe „Time limit reached“. Die 600-ms-Operation läuft weiter. Ein solcher Aufruf schließt das kombinierte Promise des aufrufenden Codes ab; er bricht die unterlegenen Operationen nicht ab.

Mit any auf den ersten Erfolg warten

Promise.any ignoriert fehlgeschlagene Eingaben, solange ein Erfolg noch möglich ist. Schlägt eine Operation nach 100 ms fehl und wird eine andere nach 200 ms erfüllt, liefert es den Wert der zweiten. Eine schnellere Ablehnung gewinnt nicht.

Werden alle Eingaben abgelehnt, wird any mit einem AggregateError abgelehnt. Sein Array errors bewahrt die Ablehnungsgründe in Eingabereihenfolge. So kann der aufrufende Code prüfen, warum jeder Versuch fehlgeschlagen ist, selbst wenn die Fehler in einer anderen Reihenfolge eingetroffen sind.

Die Entscheidungsregeln vergleichen
  • all: Jeder Wert wird benötigt; bei der ersten Ablehnung ablehnen.
  • allSettled: Jedes Ergebnis wird benötigt; auf alle Datensätze warten.
  • race: Der erste Abschluss entscheidet, auch bei einem Fehler.
  • any: Die erste Erfüllung entscheidet; nur ablehnen, wenn alle ablehnen.

Ohne Eingaben werden all und allSettled mit [] erfüllt. any wird mit AggregateError abgelehnt, weil kein Erfolg möglich ist. race bleibt ausstehend, weil nichts es abschließen kann.

Alle vier werden in aktuellen Browsern und Node.js unterstützt. all und race kamen 2015 zu ECMAScript, allSettled 2020 und any 2021.

Ein Ergebnispaar vergleichen

Angenommen, beide Operationen starten jetzt: Die erste Eingabe schlägt nach etwa 100 ms fehl, während die zweite nach etwa 300 ms mit „usable“ erfüllt wird. Wendet man jeden Kombinator auf dasselbe Paar an, ergeben sich unterschiedliche Ergebnisse:

  • All wird nach etwa 100 ms mit dem Fehler der ersten Eingabe abgelehnt, weil jede Eingabe erfolgreich sein muss.
  • AllSettled wird nach etwa 300 ms mit einem abgelehnten Datensatz, gefolgt von einem erfüllten Datensatz, erfüllt und behält dabei die Eingabepositionen bei.
  • Race wird nach etwa 100 ms abgelehnt, weil dies der erste Abschluss ist.
  • Any wird nach etwa 300 ms mit „usable“ erfüllt, weil der frühere Fehler keinen Erfolg liefern kann.

All und race werden in diesem Fall zufällig zum selben Zeitpunkt abgelehnt, folgen aber unterschiedlichen Regeln. Wäre die schnelle erste Eingabe erfolgreich und schlüge die zweite später fehl, behielte race seinen frühen Erfolg, während all beim späteren Fehler abgelehnt würde. Wähle anhand des benötigten Ergebnisses, nicht anhand des zeitlichen Ablaufs eines einzelnen Beispiels.

Wichtige Erkenntnisse

  • allSettled meldet jeden Erfolg und Fehler in Eingabereihenfolge.
  • race folgt dem ersten Abschluss, einschließlich einer Ablehnung.
  • any übernimmt den ersten Erfolg und wird nur abgelehnt, wenn jede Eingabe abgelehnt wird.
  • Eine Zeitüberschreitung mit race lässt die unterlegene Arbeit weiterlaufen.
  • race mit leerer Eingabe bleibt ausstehend; any mit leerer Eingabe wird abgelehnt.
  • Wähle den Kombinator anhand des Ergebnisses, das der aufrufende Code tatsächlich benötigt.

Asynchrones JavaScript: Beispiele

6 Interaktive Lektionen

1

Sage voraus, was jetzt und was später läuft

14 Schritte
Verfolge ein kleines Skript durch Stapel und Warteschlangen und nutze dasselbe zeitliche Modell, um Timer einzuplanen und abzubrechen.
  1. 1
    EinführungStart und Abschluss unterscheiden

    Unterscheide zunächst unmittelbare Aufrufe von registrierten Callbacks. Ein Timer kann warten, ohne den Hauptthread zu belegen, aber sein Callback benötigt diesen Thread weiterhin. Verfolge, was während des Skripts auf dem Aufrufstapel läuft, bevor du zur eingereihten Arbeit übergehst, einem Task oder einem Microtask. Deren Reihenfolge entscheidet, was du zuerst siehst.

  2. 2
    BeispielEinen Callback registrieren

    Lies dieses Skript, bevor du seine Ausgabe vorhersagst:

    console.log("request");
    setTimeout(() => console.log("ready"), 200);
    console.log("continue");
    

    Die erste Ausgabe läuft jetzt. Der Timeraufruf registriert eine Funktion und kehrt zurück, sodass das Skript sofort die letzte Ausgabe erreicht. Die Ausgabe des Callbacks gehört zu einem späteren Task; das Erstellen dieser Funktion hat sie nicht aufgerufen.

  3. 3
    KernaussageWarten belegt den Thread nicht

    Die Ausgabe beginnt mit „request“, „continue“, gefolgt von „ready“ nach etwa 200 ms. Die Hostumgebung übernimmt das Warten, aber der Callback benötigt weiterhin den Hauptthread. Eine lange Schleife kann ihn verzögern, selbst nachdem der Timer bereit ist.

  4. 4
    HerausforderungKann der Timer unterbrechen?

    Ein Skript registriert einen Timer ohne Verzögerung und führt dann eine lange synchrone Berechnung aus. Der Timer wird bereit, bevor die Berechnung endet. Kann sein Callback während dieser Berechnung laufen?

    ---reveal---

    Nein. Die Berechnung belegt den Hauptthread, bis ihre aktuelle Arbeit beendet ist. Sobald der Timer bereit ist, wird der Callback für später eingereiht; der Callback erhält dadurch weder einen separaten Ausführungsthread noch die Möglichkeit, die Berechnung zu unterbrechen.

  5. 5
    ErklärungZwei Arten späterer Arbeit unterscheiden

    Ein Timer-Callback läuft als Task. Ein Promise-Handler läuft als Microtask. Der Aufrufstapel hält die gerade ausgeführten Aufrufe fest.

    Nach der aktuellen Arbeit leert ein Microtask-Prüfpunkt die Microtask-Warteschlange, bevor ein weiterer Task beginnen kann. Alle währenddessen hinzugefügten Microtasks werden in dieselbe Warteschlange eingereiht. Führe für die Ablaufverfolgung eines Skripts getrennte Listen für unmittelbare Ausgaben, Microtasks und Timer-Tasks.

  6. 6
    BeispielDie Warteschlangen markieren

    Füge nun einen Promise-Handler hinzu:

    console.log("request");
    setTimeout(() => console.log("timer"), 0);
    Promise.resolve().then(() => console.log("job"));
    console.log("continue");
    

    Die unmittelbare Ausgabe lautet „request“, „continue“. Der Promise-Handler wartet als Microtask; der Timer-Callback ist ein Task. Beende das aktuelle Skript, bevor eine der beiden registrierten Funktionen läuft.

  7. 7
    BeispielVor dem nächsten Task die Warteschlange leeren

    Nachdem das Skript beendet ist, gibt der Microtask „job“ aus. Erst wenn die Microtask-Warteschlange leer ist, kann der bereite Timer-Task „timer“ ausgeben.

    Die vollständige Ausgabe lautet „request“, „continue“, „job“, „timer“. Ein erfülltes Promise ruft seinen Handler nicht synchron auf und ein Timer ohne Verzögerung kommt nicht vor den Microtasks an die Reihe. Der Browser kann nach dem Leeren der Warteschlange bei einer Gelegenheit zum Rendern rendern.

  8. 8
    HerausforderungEine neu eingereihte Aufgabe einordnen

    Wie lautet die Ausgabereihenfolge?

    Promise.resolve().then(() => console.log("a"))
      .then(() => console.log("c"));
    queueMicrotask(() => console.log("b"));
    console.log("now");
    

    ---reveal---

    „now“, „a“, „b“, „c“. Das Skript reiht a vor b ein. Erst wenn der Handler von a zurückkehrt, wird der nächste Handler der Kette bereit, sodass c hinter der bereits eingereihten Aufgabe b landet.

  9. 9
    ErklärungEinen Timer wählen und seine Kennung behalten

    Nutze setTimeout für einen eingeplanten Aufruf und setInterval für wiederholte Aufrufe. Beide geben eine Kennung zurück, die du speichern kannst.

    const hint = setTimeout(() => console.log("Hint"), 800);
    clearTimeout(hint);
    

    Dies bricht den Callback ab, bevor er läuft. Übergib bei einem Intervalltimer seine Kennung an clearInterval, wenn die Wiederholung enden soll. Übergib eine Funktion zum Einplanen; rufst du sie in der Argumentliste auf, wird sie sofort ausgeführt.

  10. 10
    TippVerspätete Callbacks berücksichtigen

    Verzögerungen werden in Millisekunden angegeben und legen eine Mindestwartezeit fest. Beschäftigter Code, Hintergrund-Tabs und Beschränkungen des Browsers können Callbacks weiter verzögern. Plane einen verschachtelten Timeout nach dem aktuellen Durchlauf ein; warte auch auf dessen asynchrone Arbeit, wenn sich Durchläufe nicht überschneiden dürfen.

  11. 11
    BeispielDie richtige Schleifenbindung nutzen

    Mit let gibt jeder Schleifendurchlauf seinem Callback einen eigenen Index:

    for (let n = 0; n < 3; n++) {
      setTimeout(() => console.log(n), 50);
    }
    

    Die Callbacks geben „0“, „1“, „2“ aus. Ersetzt du let durch var, teilen sie sich eine Variable, deren Wert nach der Schleife 3 ist. Alle drei geben dann „3“ aus. Der Callback liest die Bindung, wenn er später läuft.

  12. 12
    HerausforderungEinen Timer für eine ruhige Phase neu starten

    Ein Handler nutzt eine Debounce-Verzögerung von 300 ms. Eingabeereignisse treten bei 0 ms, 80 ms und 170 ms auf, danach gibt es keine weiteren Ereignisse. Wann läuft ungefähr seine eine Aktion?

    ---reveal---

    Etwa 470 ms nach dem ersten Ereignis: 300 ms nach dem letzten Ereignis bei 170 ms. Jedes Ereignis bricht den vorherigen ausstehenden Timeout ab und plant einen neuen ein. Die zuvor geplanten Aktionen laufen nicht zusätzlich und ein beschäftigter Thread könnte die letzte Aktion weiter verzögern.

  13. 13
    HerausforderungDie zeitlichen Regeln verbinden

    Ein Timer ist bereit, aber jeder Microtask reiht immer einen weiteren Microtask ein. Warum können weder der Timer-Callback noch die nächste Bildschirmaktualisierung fortfahren?

    ---reveal---

    Die Microtask-Warteschlange wird nie vollständig geleert, sodass die Ereignisschleife nicht zum nächsten Task oder zur nächsten Gelegenheit zum Rendern übergehen kann. Die abgelaufene Verzögerung des Timers setzt diese Reihenfolge nicht außer Kraft. Das Einplanen endloser Microtasks kann den Fortschritt ebenso blockieren wie eine lange Schleife.

  14. 14
    ZusammenfassungEine wiederverwendbare Ablaufverfolgung

    Verfolge zuerst den unmittelbaren Code, führe dann alle Microtasks aus und gehe anschließend zu bereiten Tasks über. Bewahre Timerkennungen zum Abbrechen auf. Verzögerungen sind ungefähre Angaben; let erhält die Werte der Schleifendurchläufe, während Debouncing eine Verzögerung für eine ruhige Phase neu startet.

2

Ein Ergebnis über einen Callback bereitstellen

9 Schritte
Erstelle eine Callback-API mit einem Ergebnis, ergänze einen Fehlerpfad und erkenne, wohin abhängige Arbeit gehört.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungDem aufrufenden Code eine nächste Aktion geben
  2. 2
    BeispielDen verzögerten Wert erzeugen
  3. 3
    VerbindungDie API steuert den Zeitpunkt
  4. 4
    HerausforderungAus welcher Funktion erfolgt die Rückkehr?
  5. 5
    BeispielEines von zwei Ergebnissen melden
  6. 6
    TippVor dem Lesen eines fehlgeschlagenen Ergebnisses stoppen
  7. 7
    HerausforderungDen aktiven Fehlerbehandlungsbereich finden
  8. 8
    HerausforderungErgebnisse und Fehler über Schritte hinweg verbinden
  9. 9
    ZusammenfassungJedes Ergebnis bis zu seiner Verarbeitung verfolgen
3

Promise-Ergebnisse erstellen und verbinden

12 Schritte
Erstelle ein Promise für eine Pause, registriere Ergebnishandler und repariere eine unterbrochene abhängige Kette.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungEin Objekt für ein späteres Ergebnis
  2. 2
    BeispielEine wiederverwendbare Pause erstellen
  3. 3
    KernaussageZustand und Auflösung auseinanderhalten
  4. 4
    HerausforderungEinen späteren Fehler richtig behandeln
  5. 5
    ErklärungDen richtigen Handler registrieren
  6. 6
    BeispielZwei Abonnements behalten einen Wert
  7. 7
    HerausforderungEin Ergebnis des Aufräumens lesen
  8. 8
    BeispielDas erste Glied erstellen
  9. 9
    BeispielDas nächste Glied warten lassen
  10. 10
    HerausforderungEine abgetrennte Operation reparieren
  11. 11
    HerausforderungAufräumen mit einer abhängigen Kette verbinden
  12. 12
    ZusammenfassungDen zurückgegebenen Pfad prüfen
4

Eine Abfolge mit async und await umschreiben

12 Schritte
Wandle einen Callback in eine Operation um, auf die mit await gewartet werden kann, und wähle die richtige Schleife für mehrere Einträge.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungPromises behalten, die Lesereihenfolge ändern
  2. 2
    BeispielEin async-Aufruf beginnt sofort
  3. 3
    HerausforderungDen Rückgabewert lesen
  4. 4
    BeispielEinen Callback mit einem Promise verbinden
  5. 5
    BeispielZwischenwerte im Gültigkeitsbereich halten
  6. 6
    HerausforderungPasst async allein das Ergebnis an?
  7. 7
    KernaussageDen Wert prüfen, nicht sein Promise
  8. 8
    BeispielforEach wartet nicht auf Callbacks
  9. 9
    TippEine Schleifenstrategie wählen
  10. 10
    HerausforderungEinen vorzeitigen Abschluss beheben
  11. 11
    HerausforderungAnpassung und Schleife verbinden
  12. 12
    ZusammenfassungBeide Seiten eines async-Aufrufs prüfen
5

Fehler, Fortsetzung und Aufräumen verbunden halten

13 Schritte
Verfolge eine fehlgeschlagene Aktion durch catch, ergänze eine lokale Rückfalllösung und erstelle eine Strategie mit begrenzten Wiederholungsversuchen.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungEntscheiden, was ein Fehler bedeuten soll
  2. 2
    BeispielDie Fortsetzung nach einem catch beobachten
  3. 3
    KernaussageAuch Protokollieren wählt einen Rückgabewert
  4. 4
    HerausforderungDas fehlgeschlagene Ergebnis erhalten
  5. 5
    BeispielDort warten, wo catch den Fehler erkennen kann
  6. 6
    TippNach lokaler Behandlung zurückkehren
  7. 7
    HerausforderungDie lokale Rückfalllösung wählen
  8. 8
    VerbindungDen Fehlerpfad der Aktion abschließen
  9. 9
    BeispielKontext und ursprüngliche Ursache bewahren
  10. 10
    BeispielEinen neuen Aufruf begrenzt wiederholen
  11. 11
    HerausforderungNach Erfolg stoppen
  12. 12
    HerausforderungFortsetzung und Aufräumen verbinden
  13. 13
    ZusammenfassungJedem Ergebnis ein Ziel geben
6

Festlegen, was eine Gruppe von Aufgaben liefern muss

11 Schritte
Lass unabhängige Wartezeiten überlappen und wähle den Kombinator, der zum benötigten Ergebnis passt.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungMit der Frage nach Abhängigkeiten beginnen
  2. 2
    BeispielBeide Operationen gemeinsam starten
  3. 3
    KernaussageEin Fehler bricht die Gruppe nicht ab
  4. 4
    HerausforderungEine Abhängigkeit beachten, den Rest überlappen lassen
  5. 5
    ErklärungJedes Ergebnis behalten, wenn Teile nützlich bleiben
  6. 6
    BeispielErsten Abschluss und ersten Erfolg unterscheiden
  7. 7
    HerausforderungFür eine Seite mit Teilergebnissen wählen
  8. 8
    TippEine Zeitüberschreitung beendet das Warten, nicht die Operation
  9. 9
    HerausforderungWas kann eine leere Gruppe abschließen?
  10. 10
    HerausforderungStartzeitpunkt und Fehlerbehandlung verbinden
  11. 11
    ZusammenfassungDie Ergebnisstrategie wählen

Asynchrones JavaScript: Übungsaufgaben

40 Übungsfragen in 4 Sets

Set 1: Verfolge den Code von Aufrufen bis zu Callbacks10 Übungsfragen

Frage 1 von 10

Eine Seite registriert einen Timer und führt dann eine lange synchrone Berechnung aus. Der Timer wird während der Berechnung bereit. Wann kann sein Callback laufen?
  1. Sobald die angeforderte Verzögerung des Timers abgelaufen ist.
  2. Während des nächsten Durchlaufs der Berechnungsschleife.
  3. Nachdem die Berechnung den Hauptthread freigegeben hat.
  4. Auf einem zweiten Thread, den der Timeraufruf erstellt.
Antwort zeigenAntwort ausblenden
Antwort: C
Die Berechnung belegt den Hauptthread bis zu ihrem Ende, sodass der bereite Callback warten muss. Der Ablauf der Timerverzögerung kann laufendes JavaScript nicht unterbrechen.

Frage 2 von 10

Wie lautet die Ausgabereihenfolge?

console.log("open");
setTimeout(() => console.log("tick"), 0);
Promise.resolve().then(() => console.log("job"));
console.log("close");
  1. "open", "close", "job", "tick"
  2. "open", "tick", "job", "close"
  3. "open", "job", "close", "tick"
  4. "open", "close", "tick", "job"
Antwort zeigenAntwort ausblenden
Antwort: A
Das aktuelle Skript gibt „open“ und „close“ aus, bevor ein registrierter Callback läuft. Sein Promise-Handler ist ein Microtask und gibt daher „job“ aus, bevor der Timer-Task „tick“ ausgibt. Die Verzögerung von null macht den Timer für eine spätere Ausführung bereit; sie lässt ihn nicht vor dem Leeren der Microtask-Warteschlange laufen.

Frage 3 von 10

Ein Timer wurde mit const reminder = setTimeout(showHint, 900); eingeplant. Welche Anweisung bricht ihn ab, bevor er läuft?
  1. clearTimeout(showHint);
  2. setTimeout(reminder, 0);
  3. clearInterval(showHint);
  4. clearTimeout(reminder);
Antwort zeigenAntwort ausblenden
Antwort: D
Die Funktion clearTimeout identifiziert den Timer anhand seiner zurückgegebenen Kennung reminder. Die Übergabe von showHint liefert die Funktion, die diesen eingeplanten Timer nicht identifiziert.

Frage 4 von 10

Was gibt diese Schleife aus, nachdem ihre Callbacks gelaufen sind?

for (var index = 2; index < 5; index++) {
  setTimeout(() => console.log(index), 0);
}
  1. 2, 3, 4
  2. 5, 5, 5
  3. 2, 2, 2
  4. 3, 4, 5
Antwort zeigenAntwort ausblenden
Antwort: B
Alle Callbacks schließen dieselbe var-Bindung ein. Wenn sie laufen, ist die Schleife bereits bei einem index-Wert von 5 beendet, sodass alle drei Callbacks 5 ausgeben. Die Folge 2, 3, 4 entstünde bei einer eigenen let-Bindung pro Schleifendurchlauf, nicht bei dieser gemeinsam genutzten Variablen.

Frage 5 von 10

Wie viele Callback-Aufrufe sind abgeschlossen, wenn dieser Code die Ausgabe erreicht?

let calls = 0;
[3, 6, 9].map(n => {
  calls++;
  return n + 1;
});
console.log(calls);
  1. Drei Aufrufe sind abgeschlossen.
  2. Kein Aufruf ist abgeschlossen.
  3. Nur ein Aufruf ist abgeschlossen.
  4. Zwei Aufrufe sind abgeschlossen.
Antwort zeigenAntwort ausblenden
Antwort: A
Map ruft alle drei Callbacks auf, bevor es zurückkehrt. Daher ist calls bei der Ausgabe 3. Die Übergabe eines Callbacks verschiebt ihn nicht auf später, wenn seine API ihn synchron aufruft.

Frage 6 von 10

Was enthält result, wenn die Ausgabe läuft?

function make() {
  setTimeout(() => { return "badge"; }, 0);
}
const result = make();
console.log(result);
  1. Die Zeichenkette "badge".
  2. Ein mit "badge" erfülltes Promise.
  3. undefined
  4. Die Timer-Callback-Funktion.
Antwort zeigenAntwort ausblenden
Antwort: C
Die Funktion make hat keine return-Anweisung und gibt undefined zurück. Das spätere return beendet nur den Timer-Callback; es kann keinen Wert an einen bereits beendeten äußeren Aufruf liefern.

Frage 7 von 10

Ein Error-first-Callback erhält bei einem Fehler ein Error-Objekt und kein Ergebnis. Was verhindert, dass der Erfolgscode nach dieser Fehlerprüfung ein fehlendes item liest?

if (err) {
  console.log(err.message);
  // Add one statement here.
}
console.log(item.name);
  1. err = null;
  2. return;
  3. console.log(item);
  4. callback = null;
Antwort zeigenAntwort ausblenden
Antwort: B
Die Rückkehr verlässt diesen Callback, nachdem er den Fehler gemeldet hat, sodass die Zeile item.name im Fehlerfall nicht erreicht wird. Das Setzen von err auf null ändert nur die lokale Variable; es erzeugt kein erfolgreiches Ergebnis. Der Fehlerzweig muss die normale Erfolgsverarbeitung stoppen, wenn kein item existiert.

Frage 8 von 10

Wie lautet die Ausgabereihenfolge?

Promise.resolve().then(() => {
  console.log("oak");
  queueMicrotask(() => console.log("elm"));
}).then(() => console.log("ash"));
queueMicrotask(() => console.log("fir"));
console.log("pine");
  1. "pine", "oak", "elm", "ash", "fir"
  2. "pine", "fir", "oak", "elm", "ash"
  3. "pine", "oak", "fir", "ash", "elm"
  4. "pine", "oak", "fir", "elm", "ash"
Antwort zeigenAntwort ausblenden
Antwort: D
Das Skript gibt „pine“ aus und reiht zunächst oak vor fir ein. Der Handler für oak fügt elm hinzu; seine Rückkehr macht dann den verketteten Handler für ash bereit, sodass beide in dieser Reihenfolge hinter fir eingereiht werden. Der Handler für ash kann nicht vor elm laufen, weil elm während des Handlers eingereiht wurde, bevor das zurückgegebene Promise erfüllt wurde.

Frage 9 von 10

Eine Operation mit einem Ergebnis nutzt diesen Timerrumpf. Was geschieht, wenn id gleich 0 ist, und welche Änderung behebt das Problem?

if (id <= 0) callback(new Error("Invalid"));
callback(null, { id });
  1. Er ruft einmal auf; verschiebe den Erfolgsaufruf vor die Prüfung.
  2. Er ruft zweimal auf; entferne das Fehlerargument aus dem ersten Aufruf.
  3. Er ruft zweimal auf; kehre nach dem Melden des Fehlers zurück.
  4. Er ruft einmal auf; füge nach dem Erfolgsaufruf ein return hinzu.
Antwort zeigenAntwort ausblenden
Antwort: C
Die erfüllte Fehlerbedingung führt den ersten Callback aus, aber keine Kontrollflussanweisung stoppt den zweiten Aufruf. Ein return im Fehlerzweig verhindert dieses Weiterlaufen und erhält die Vereinbarung über ein Ergebnis. Eine Rückkehr nach dem Erfolgsaufruf ist zu spät, weil beide Aufrufe dann bereits erfolgt sind.

Frage 10 von 10

Wie lautet die Ausgabereihenfolge?

console.log("outside");
const p = new Promise(resolve => {
  console.log("executor");
  resolve("value");
});
p.then(console.log);
console.log("registered");
  1. "outside", "executor", "registered", "value"
  2. "outside", "registered", "executor", "value"
  3. "outside", "executor", "value", "registered"
  4. "executor", "outside", "registered", "value"
Antwort zeigenAntwort ausblenden
Antwort: A
Der Promise-Konstruktor führt seine Executor-Funktion während der Erstellung zwischen den beiden umgebenden synchronen Ausgaben aus. Der then-Handler läuft später als Microtask und gibt „value“ zuletzt aus. Die sofortige Erfüllung des Promises führt nicht dazu, dass ein mit then registrierter Handler synchron läuft.
Set 2: Gib Werte durch Promises und await weiter10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Welchen Wert liefert dieses Promise an seinen Erfüllungshandler?

const p = new Promise((resolve, reject) => {
  resolve(14);
  reject(new Error("Changed"));
  resolve(28);
});
  1. 28
  2. 14
  3. Das Error-Objekt mit der Meldung "Changed".
  4. undefined
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Was wird ausgegeben?

const p = Promise.resolve(7);
p.then(n => n + 4);
p.then(n => console.log(n));
  1. 11
  2. undefined
  3. Ein Promise-Objekt.
  4. 7
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Was wird ausgegeben?

Promise.resolve(6)
  .then(n => n - 2)
  .then(n => n * 5)
  .then(console.log);
  1. 20
  2. 30
  3. 4
  4. undefined
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Ein Promise zurückgebendes wait(ms) wird nach etwa ms Millisekunden erfüllt. Was erreicht console.log und wann?

wait(120)
  .then(() => { wait(480).then(() => "ready"); })
  .then(console.log);
  1. "ready" nach etwa 600 ms.
  2. undefined nach etwa 600 ms.
  3. undefined nach etwa 120 ms.
  4. "ready" nach etwa 480 ms.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Welche Art von Wert gibt der Aufruf dieser Funktion zurück?

async function count() { return 9; }
  1. Direkt die Zahl 9.
  2. Ein mit undefined erfülltes Promise.
  3. Eine Funktion, die 9 zurückgeben wird.
  4. Ein mit 9 erfülltes Promise.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Wie lautet die Ausgabereihenfolge?

async function show() {
  console.log("inside");
  await 0;
  console.log("after await");
}
show();
console.log("caller");
  1. "caller", "inside", "after await"
  2. "inside", "caller", "after await"
  3. "inside", "after await", "caller"
  4. "caller", "after await", "inside"
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Eine Callback-API ruft done(err, value) einmal auf. Was sollte ihr Promise-Wrapper tun, wenn err ein Error-Objekt ist?
  1. Resolve mit err aufrufen und den Callback beenden.
  2. Err auslösen, nachdem der verzögerte Callback zurückkehrt.
  3. Reject mit err aufrufen und den Callback beenden.
  4. Resolve mit value und dann reject mit err aufrufen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Was wird ausgegeben?

async function permitted() { return false; }
if (permitted()) console.log("enter");
else console.log("stop");
  1. "enter"
  2. "stop"
  3. false
  4. Nichts, bis das Promise abgeschlossen ist.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

record(id) gibt ein Promise zurück und kann abgelehnt werden. Wie sollte diese Funktion Kennungen der Reihe nach verarbeiten, bei einem Fehler stoppen und dem aufrufenden Code ermöglichen, diesen Fehler abzufangen?

async function saveAll(ids) {
  ids.forEach(async id => { await record(id); });
}
  1. Innerhalb der Funktion auf den vorhandenen forEach-Aufruf warten.
  2. Await vor record im vorhandenen asynchronen Callback entfernen.
  3. Map mit asynchronen Callbacks nutzen, aber das zurückgegebene Array ignorieren.
  4. Die Methode forEach durch for...of ersetzen und innerhalb der Schleife auf record warten.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Welchen Wert gibt der letzte Handler aus?

Promise.resolve(11)
  .finally(() => 40)
  .then(n => n + 2)
  .finally(() => Promise.resolve(90))
  .then(console.log);
  1. 42
  2. 13
  3. 90
  4. undefined
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 3: Behandle Fehler und kombiniere unabhängige Aufgaben10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Ein catch-Handler gibt nach einer Ablehnung "default" zurück. Was erhält der folgende then-Handler?
  1. Den ursprünglichen Ablehnungsgrund.
  2. Trotz der Rückgabe den Wert undefined.
  3. Ein abgelehntes Promise mit "default".
  4. Den erfüllten Wert "default".
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Was wird ausgegeben?

Promise.resolve("start")
  .then(() => { throw new Error("later"); },
        () => console.log("same call"))
  .catch(err => console.log(err.message));
  1. Nur "same call".
  2. Nur "later".
  3. "same call", dann "later".
  4. "start", dann "later".
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Wie lautet die Ausgabereihenfolge?

async function run() {
  try {
    await Promise.reject(new Error("busy"));
    console.log("saved");
  } catch (err) {
    console.log(err.message);
  } finally {
    console.log("reset");
  }
}
run();
  1. "saved", "reset"
  2. "reset", "busy"
  3. "busy", "reset"
  4. "busy", "saved", "reset"
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Was gibt der aufrufende Code aus?

async function read() {
  try {
    return Promise.reject(new Error("missing"));
  } catch {
    return "local";
  }
}
read().then(console.log).catch(err => console.log(err.message));
  1. "missing"
  2. "local"
  3. undefined
  4. Nichts, weil die Funktion ausstehend bleibt.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Eine Funktion verpackt einen Fehler als new Error("Report failed", { cause: original }). Welche Eigenschaft hält das ursprüngliche Error-Objekt verfügbar?
  1. message
  2. name
  3. cause
  4. status
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Eine Hilfsfunktion für Wiederholungsversuche wartet in try auf eine neue Aufgabe, kehrt bei Erfolg zurück und wiederholt sie in catch bis zu ihrer Grenze von vier Aufrufen. Die Aufgabe wird bei den Aufrufen 1 und 2 abgelehnt und beim Aufruf 3 mit "ok" erfüllt. Was geschieht?
  1. Sie führt vier Aufrufe aus und wird mit "ok" erfüllt.
  2. Sie führt einen Aufruf aus und wird mit dem ersten Fehler abgelehnt.
  3. Sie führt drei Aufrufe aus und wird mit dem zweiten Fehler abgelehnt.
  4. Sie führt drei Aufrufe aus und wird mit "ok" erfüllt.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Zwei unabhängige timerbasierte Funktionen benötigen etwa 350 ms und 900 ms. Beide werden im Eingabearray von await Promise.all(...) aufgerufen und beide sind erfolgreich. Wie lange dauert das gemeinsame Warten ungefähr?
  1. Etwa 900 ms.
  2. Etwa 1250 ms.
  3. Etwa 350 ms.
  4. Etwa 550 ms.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Aufgaben werden gemeinsam gestartet: A wird nach 800 ms erfüllt, B nach 250 ms abgelehnt und C protokolliert einen Seiteneffekt und wird nach 600 ms erfüllt. Was tut Promise.all([A, B, C])?
  1. Ablehnung nach etwa 800 ms; C kann nach etwa 600 ms ausgeben.
  2. Ablehnung nach etwa 250 ms; C kann nach etwa 600 ms ausgeben.
  3. Ablehnung nach etwa 250 ms; C wird vor seiner Ausgabe abgebrochen.
  4. Erfüllung nach etwa 800 ms mit A und C, aber ohne Ergebnis von B.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Unabhängige Bereiche müssen nach Abschluss aller Versuche jeweils entweder ihr Ergebnis oder ihren eigenen Fehler anzeigen. Welcher Promise-Kombinator liefert die benötigten Datensätze?
  1. Promise.allSettled
  2. Promise.all
  3. Promise.race
  4. Promise.any
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Zwei Promises beginnen gemeinsam: Eines wird nach etwa 150 ms abgelehnt und eines nach etwa 400 ms mit "backup" erfüllt. Wie unterscheiden sich race und any?
  1. Race wird nach etwa 400 ms erfüllt; any nach etwa 150 ms abgelehnt.
  2. Beide werden nach etwa 150 ms mit dem ersten Fehler abgelehnt.
  3. Race wird nach etwa 150 ms abgelehnt; any nach etwa 400 ms erfüllt.
  4. Beide werden nach etwa 400 ms mit dem Ersatzwert erfüllt.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 4: Durchgängige asynchrone Abläufe debuggen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Was läuft zuerst, wenn diese Anweisung ausgewertet wird?

setTimeout(showMessage(), 500);
  1. Der Aufruf von showMessage läuft sofort.
  2. Der Timer wartet 500 ms vor showMessage.
  3. Der Timer reiht showMessage als Microtask ein.
  4. Ein ausstehendes Promise muss zuerst abgeschlossen werden.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Ein async-Wrapper startet eine Error-first-Callback-Operation, gibt aber kein mit ihrem Callback verbundenes Promise zurück. Sein Rumpf erreicht sofort das Ende. Was ergibt das Warten auf den Wrapper?
  1. Den späteren Erfolgswert des Callbacks.
  2. Das spätere Error-Objekt des Callbacks.
  3. Undefined, bevor das Callback-Ergebnis vorliegt.
  4. Eine Wartezeit bis zum Aufruf des Callbacks.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

inner ist noch ausstehend. Was geschieht hier, wenn inner später abgelehnt wird?

const outer = new Promise((resolve, reject) => {
  resolve(inner);
  reject(new Error("replacement"));
});
outer.catch(err => console.log(err.message));
  1. Outer wird sofort mit "replacement" abgelehnt.
  2. Outer übernimmt den Ablehnungsgrund von inner.
  3. Outer wird mit dem Promise-Objekt inner erfüllt.
  4. Outer bleibt ausstehend, nachdem inner abgelehnt wurde.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Wie lautet die Ausgabereihenfolge?

async function step() {
  console.log("a");
  await null;
  console.log("b");
  queueMicrotask(() => console.log("c"));
}
setTimeout(() => console.log("d"), 0);
step();
Promise.resolve().then(() => console.log("e"));
console.log("f");
  1. "a", "f", "e", "b", "c", "d"
  2. "a", "f", "b", "c", "e", "d"
  3. "a", "b", "f", "e", "c", "d"
  4. "a", "f", "b", "e", "c", "d"
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Was gibt der letzte Handler aus?

Promise.reject(new Error("first"))
  .catch(() => 4)
  .then(n => { throw new Error("second " + n); })
  .finally(() => 9)
  .catch(err => console.log(err.message));
  1. "first"
  2. "second 4"
  3. 9
  4. "second 9"
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Ein Kontoabruf dauert etwa 200 ms. Nach seiner Erfüllung dauern zwei unabhängige kontoabhängige Aufrufe etwa 500 ms und 800 ms. Beide benötigen das Konto und alle Ergebnisse werden benötigt. Welcher Plan liefert das vollständige Ergebnis am frühesten?
  1. Auf das Konto warten, dann beide späteren Aufrufe mit Promise.all starten; etwa 1000 ms.
  2. Alle drei sofort in Promise.all starten; etwa 800 ms.
  3. Auf das Konto warten, dann auf jeden späteren Aufruf einzeln warten; etwa 1500 ms.
  4. Auf das Konto warten, dann die späteren Aufrufe mit race kombinieren; etwa 700 ms.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Warum wiederholt diese Funktion eine asynchron abgelehnte Aufgabe nicht?

async function retry(task) {
  for (let n = 0; n < 3; n++) {
    try { return task(); }
    catch (err) { if (n === 2) throw err; }
  }
}
  1. Sie fängt die Ablehnung ab, endet aber, weil die Schleife bei null beginnt.
  2. Sie kann nicht wiederholen, weil task als Funktion statt als Promise übergeben wird.
  3. Sie wiederholt dreimal, bevor ein zurückgegebenes Promise abgeschlossen sein kann.
  4. Sie gibt das Promise zurück, bevor dessen Ablehnung das lokale catch erreichen kann.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Welchen Zustand hat Promise.race([]), wenn niemals Eingaben hinzugefügt werden?
  1. Es wird mit [] erfüllt.
  2. Es wird sofort abgelehnt.
  3. Es bleibt ausstehend.
  4. Es wird mit undefined aufgelöst.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Zwei Versuche starten gemeinsam. Die zweite Eingabe wird zuerst abgelehnt; die erste Eingabe wird später abgelehnt. Was stellt Promise.any([first, second]) schließlich für catch bereit?
  1. Das Error-Objekt der zweiten Eingabe, sobald sie abgelehnt wird.
  2. Einen AggregateError mit den Gründen in der Reihenfolge erste, zweite Eingabe.
  3. Einen AggregateError mit den Gründen in der Reihenfolge zweite, erste Eingabe.
  4. Nur das Error-Objekt der ersten Eingabe nach der späteren Ablehnung.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Was gibt der aufrufende Code aus?

async function value() {
  try {
    return await Promise.reject(new Error("lost"));
  } catch {
    return "fallback";
  } finally {
    console.log("cleanup");
  }
}
value().then(console.log);
console.log("caller");
  1. "cleanup", "caller", "fallback"
  2. "caller", "fallback", "cleanup"
  3. "caller", "cleanup", "lost"
  4. "caller", "cleanup", "fallback"
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe

Schlüsselbegriffe

72 Schlüsselbegriffe, jeweils mit Definition

A

Abgelehnt
Abgelehnt ist der abgeschlossene Zustand eines Promises, das einen Fehlergrund enthält.
Eine fehlgeschlagene Bestandsprüfung wird mit einem Error-Objekt abgelehnt, dessen Meldung erklärt, warum die angeforderte Menge nicht verfügbar ist.
Abgeschlossen
Ein abgeschlossenes Promise wurde erfüllt oder abgelehnt; sein Zustand und sein Ergebnis stehen damit fest.
Ein erneuter Aufruf von resolve, nachdem ein Promise erfüllt wurde, ersetzt nicht den Wert, den seine Handler erhalten.
Abhängige Operation
Eine abhängige Operation benötigt das Ergebnis einer früheren Operation und darf erst starten, wenn diese benötigte Eingabe verfügbar ist.
Ein Callback erhält einen Nutzer und startet dann die nächste Operation mit diesem Nutzer als benötigtem Argument.
Ablehnungshandler
Ein Ablehnungshandler erhält den Fehlergrund eines Promises und kann die Verarbeitung mit einem Wert fortsetzen oder durch Auslösen eines Fehlers den Fehlerzustand erhalten.
Ein catch-Handler liefert Standardeinstellungen, wenn eine fehlgeschlagene Operation für Einstellungen die größere Aktion nicht stoppen soll.
AggregateError
Ein AggregateError fasst mehrere Fehler zusammen; Promise.any nutzt ihn mit einem Array errors in Eingabereihenfolge, wenn alle Versuche fehlschlagen.
Schlagen zwei Versuche in umgekehrter Reihenfolge fehl, listet das Array errors ihre Gründe weiterhin in der ursprünglichen Eingabereihenfolge auf.
async-Funktion
Eine async-Funktion gibt immer ein Promise für ihr zurückgegebenes Ergebnis oder ihren ausgelösten Fehler zurück.
Der Aufruf einer async-Funktion, die eine Zahl zurückgibt, liefert dem aufrufenden Code ein Promise, selbst ohne await im Funktionsrumpf.
Asynchrone Operation
Eine asynchrone Operation startet jetzt und liefert ihr Ergebnis später, während anderer Code weiterlaufen kann.
Ein Timer wird jetzt registriert, während sein Callback eine Meldung ausgibt, nachdem die Umgebung die Verzögerung übernommen hat.
Asynchroner Callback
Ein asynchroner Callback wird später durch zeitliche Planung aufgerufen, nachdem die Operation, die ihn registriert hat, zum aufrufenden Code zurückgekehrt ist.
Eine Funktion zur Vorbereitung einer Beschriftung kehrt zuerst zurück; ihr Ergebnis-Callback erhält später die fertige Beschriftung.
Aufgelöst
Bei einem aufgelösten Promise steht das Ergebnis fest, möglicherweise durch die Übernahme eines anderen Promises, das noch aussteht und später abgelehnt werden kann.
Wird ein Wrapper mit einem ausstehenden Promise aufgelöst, folgt er dessen späterem Erfolg oder Fehler.
Aufrufstapel
Der Aufrufstapel hält aktive Funktionsaufrufe fest, wobei der aktuelle Aufruf oben liegt.
Ruft ein Handler eine Hilfsfunktion auf, läuft diese oben auf dem Stapel, bis sie zum Handler zurückkehrt.
Auslösen einer Ausnahme
Das Auslösen einer Ausnahme unterbricht an dieser Stelle den normalen Ablauf und überträgt einen Fehler an ein passendes catch oder gibt ihn nach außen weiter, wenn keines zutrifft.
Ein abgelehntes, mit await erwartetes Promise verhält sich wie eine Ausnahme an seiner await-Zeile und überspringt die verbleibenden Anweisungen in try.
Ausstehend
Ausstehend ist der Zustand eines Promises vor seiner Erfüllung oder Ablehnung.
Eine timerbasierte Pause bleibt ausstehend, während die Umgebung darauf wartet, die Funktion aufzurufen, die sie auflöst.
await
await hält seine async-Funktion an, bis ein Ergebnis vorliegt, und liefert an dieser Zeile einen Erfüllungswert oder löst den Ablehnungsgrund aus.
Das Warten auf eine Pause lässt den aufrufenden Code eine Meldung ausgeben, bevor die angehaltene Funktion ihre nächste Anweisung fortsetzt.

B

Blockierung
Bei einer Blockierung belegt Arbeit einen Thread und verhindert, dass anderer eingeplanter Code darauf läuft.
Eine beschäftigte Schleife kann einen Timer ohne Verzögerung aufhalten, weil der Callback weiterhin den belegten Hauptthread benötigt.

C

Callback
Ein Callback ist eine Funktion, die an eine andere Funktion übergeben wird, damit diese sie aufruft.
Die Übergabe eines benannten Handlers stellt eine Aktion bereit; zusätzliche Klammern würden ihn während der Auswertung des Arguments aufrufen.
Callback-Hölle
Die Callback-Hölle ist tief verschachtelter Callback-Code, bei dem jeder abhängige Schritt eine weitere Ebene und häufig einen weiteren Fehlerbehandlungszweig hinzufügt.
Das Lesen eines Profils, dann der Projekte und dann der Aufgaben erzeugt mehrere verschachtelte Callbacks, die schwer zu überblicken sind.

D

Debouncing
Debouncing startet bei jedem Ereignis einen Timer neu, sodass eine Aktion auf eine ruhige Phase wartet.
Die schnelle Eingabe mehrerer Zeichen ersetzt jedes Mal den ausstehenden Timer, sodass nach Ende der Eingabe eine Aktualisierung erfolgt.

E

Eingabereihenfolge
Die Eingabereihenfolge ist die Anordnung der Werte oder Promises, die einem Kombinator übergeben werden.
Die Destrukturierung des Promise.all-Ergebnisses benennt Werte entsprechend den Positionen des ursprünglichen Arrays.
Ereignisschleife
Die Ereignisschleife koordiniert Tasks, Microtask-Prüfpunkte und Gelegenheiten zum Rendern, sodass bereite Arbeit geordnet ausgeführt werden kann.
Verfolgst du zuerst ein Skript und danach seine Microtasks, erklärt das, warum seine Promise-Meldung vor seiner Timer-Meldung erscheint.
Erfüllt
Erfüllt ist der abgeschlossene Zustand eines Promises, das einen erfolgreichen Wert enthält.
Eine erfolgreiche Mengenprüfung wird mit der verbleibenden Anzahl erfüllt und ihr Erfolgshandler erhält diese Zahl.
Erfüllungshandler
Ein Erfüllungshandler ist eine mit then registrierte Funktion, die den Erfolgswert eines Promises erhält und das Ergebnis eines neuen Promises bestimmt.
Ein Handler multipliziert die erhaltene Anzahl und gibt die neue Anzahl für das nächste Glied der Kette zurück.
Erneutes Auslösen einer Ausnahme
Das erneute Auslösen einer Ausnahme löst einen abgefangenen Fehler erneut aus, sodass ein späterer Handler ihn erhalten kann.
Ein catch auf niedrigerer Ebene protokolliert einen Fehler und löst ihn erneut aus, sodass die äußere Aktion für die Nutzermeldung verantwortlich bleibt.
Error-first-Callback
Ein Error-first-Callback erhält vor seinem Ergebnis ein Fehlerargument: null bei Erfolg und ein Error-Objekt bei einem Fehler.
Der aufrufende Code prüft den Fehler, bevor er den Eintrag liest, sodass eine fehlgeschlagene Operation keine Erfolgsverarbeitung auslöst.
Error-Objekt
Ein Error-Objekt stellt einen Fehler mit Meldung und Namen dar und enthält üblicherweise Stapelinformationen, die beim Auffinden des Problems helfen.
Das Lesen der Meldung eines abgefangenen Error-Objekts mit der Meldung „Missing label“ liefert diesen Text, ohne dass die Benutzeroberfläche seinen Stacktrace anzeigen muss.
Executor-Funktion
Eine Executor-Funktion ist die Funktion, die der Promise-Konstruktor sofort mit resolve und reject aufruft.
Die Executor-Funktion registriert sofort einen Timer; der Erfolgshandler läuft dennoch später über die Microtask-Warteschlange.

F

Fail-fast-Verhalten
Das Fail-fast-Verhalten lehnt ein gemeinsames Warten ab, sobald eine benötigte Eingabe fehlschlägt, ohne auf den Abschluss der übrigen Eingaben zu warten.
Eine zusammengefasste Vorbereitung schlägt fehl, wenn ein Teil nach 200 ms fehlschlägt, obwohl ein anderer Teil noch auf seinen späteren Abschluss hinarbeitet.
Fehlende Rückgabe
Eine fehlende Rückgabe lässt das Ergebnis eines Handlers undefined sein und kann Arbeit abtrennen, die er zurückgeben sollte.
Ein Handler mit Blockrumpf startet eine Pause, vergisst aber, sie zurückzugeben. Daher läuft der nächste Handler, bevor die Pause endet.
Fehlerbehandlungsbereich
Ein Fehlerbehandlungsbereich umfasst die verbundene Arbeit, deren Fehler einen bestimmten catch-Handler erreichen können. Er wird durch dessen Platzierung und die mit await erwarteten Aufrufe bestimmt.
Ein catch in der Mitte behandelt frühere Schritte, kann aber keinen neuen Fehler eines späteren Handlers behandeln.
Fehlerereignis
Ein Fehlerereignis meldet einen Fehler über einen Event-Handler, sodass eine Browser-API einen vom Erfolgsereignis getrennten Fehlerpfad nutzen kann.
Bei einem Bild werden Fehlerhandler und Erfolgshandler zugewiesen, bevor das Laden der Quelle beginnt.
Fehlerursache
Eine Fehlerursache ist ein ursprünglicher Fehler, der in der Eigenschaft cause eines neuen Error-Objekts bewahrt wird.
Ein äußerer Fehler mit der Meldung „Report failed“ enthält in seiner Ursache weiterhin den Gerätefehler, der die Vorbereitung des Berichts verhindert hat.
finally-Block
Ein finally-Block läuft beim Verlassen seiner try/catch-Struktur und ermöglicht das Aufräumen nach Erfolg, Fehler oder einer return-Anweisung.
Eine Schaltfläche wird in finally aktiviert, sodass die Oberfläche nach erfolgreichen wie auch fehlgeschlagenen Versuchen für eine weitere Aktion bereit ist.
finally-Callback
Ein finally-Callback läuft nach beiden möglichen Ergebnissen eines Promises, erhält kein Argument und bewahrt normalerweise den früheren Wert oder Grund.
Der Callback blendet nach Erfolg oder Fehler eine Ladeanzeige aus; eine Ausnahme beim Aufräumen würde zu einem neuen Fehler werden.
Flache Promise-Kette
Eine flache Promise-Kette drückt abhängige Promise-Schritte als aufeinanderfolgende Handler auf der äußeren Ebene aus und hält ihren zeitlichen Ablauf und ihre Fehlerweitergabe verbunden.
Die Rückgabe jeder neuen Operation lässt ein catch am Ende Fehler aus den verbundenen Schritten davor erhalten.
Fortsetzung
Eine Fortsetzung ist der Code in einer async-Funktion, der nach einem await weiterläuft.
Selbst das Warten auf eine gewöhnliche Zahl verschiebt die verbleibenden Zeilen, statt sie vor der Fortsetzung des aufrufenden Codes auszuführen.

H

Hauptthread
Der Hauptthread führt das gewöhnliche JavaScript der Seite und bildschirmbezogene Aufgaben aus.
Eine lange Berechnung auf dem Hauptthread verhindert, dass ein bereiter Klick-Handler läuft, bis die Berechnung beendet ist.
Hostumgebung
Die Hostumgebung ist der Browser oder die Node.js-Laufzeitumgebung, die Funktionen wie Timer bereitstellt und deren zeitliche Planung übernimmt.
Der Browser überwacht die Verzögerung eines Timers und stellt dann seinen Callback zur späteren Ausführung durch JavaScript bereit.

I

Ignoriertes Callback-Promise
Ein ignoriertes Callback-Promise ist ein von einem Callback zurückgegebenes Promise, das die aufrufende API nicht nutzt, um Abschluss oder Fehler zu verfolgen.
Die Methode forEach ignoriert das Promise eines asynchronen Callbacks, sodass die umgebende Funktion vor dieser Arbeit enden kann.
Intervalltimer
Ein Intervalltimer plant wiederholt einen Timer-Callback ein, bis er abgebrochen wird.
Ein Zähler bricht seinen Intervalltimer nach dem fünften Durchlauf ab, sodass die Seite keine weiteren Meldungen zu Durchläufen erzeugt.

M

Microtask
Ein Microtask ist eine eingereihte Aufgabe, etwa ein Promise-Handler, die an einem Microtask-Prüfpunkt läuft, bevor die Ereignisschleife einen weiteren Task startet.
Der Handler eines erfüllten Promises läuft nach dem aktuellen Skript, aber vor einem bereiten Timer-Task ohne Verzögerung.
Microtask-Starvation
Microtask-Starvation verzögert Tasks und das Rendern, weil die Microtask-Warteschlange nie vollständig geleert wird.
Ein Microtask, der immer eine weitere Kopie seiner selbst einreiht, kann verhindern, dass ein Klick-Handler jemals an die Reihe kommt.

N

Nebenläufigkeit
Nebenläufigkeit ermöglicht es unabhängigen Operationen, während überlappender Wartezeiten gleichzeitig im Gang zu bleiben, während ihre JavaScript-Handler weiterhin einzeln ausgeführt werden.
Zwei Timer beginnen gemeinsam, sodass der aufrufende Code etwa so lange wie auf den langsameren Timer wartet, statt beide Verzögerungen zu addieren.

P

Promise
Ein Promise ist ein Objekt, das für ein späteres Ergebnis steht und verbundenen Handlern einen Wert oder einen Fehlergrund liefern kann.
Eine Funktion gibt sofort ein Promise zurück; später liefert ein Timer den Wert, auf den der aufrufende Code wartet.
Promise-Abonnement
Ein Promise-Abonnement registriert einen Handler an einem bestimmten Promise.
Zwei Handler an einem erfüllten Promise erhalten jeweils dessen ursprüngliche Zahl, selbst wenn einer eine veränderte Zahl zurückgibt.
Promise-Adoption
Bei der Promise-Adoption folgt ein Promise dem späteren Erfüllungswert oder Ablehnungsgrund eines anderen Promises, statt es als gewöhnliches Objekt zu behandeln.
Die Rückgabe eines Promises aus then lässt den nächsten Handler warten und dessen Wert ohne ein zusätzliches verschachteltes Promise erhalten.
Promise-Kette
Eine Promise-Kette verbindet Handler über zurückgegebene Promises und gibt jeden Rückgabewert oder jedes übernommene asynchrone Ergebnis an das nächste Glied weiter.
Ein Handler gibt ein Promise für die Vorbereitung zurück, sodass der nächste Handler den fertigen Eintrag nach Abschluss der Vorbereitung erhält.
Promise-Wrapper
Ein Promise-Wrapper stellt das Ergebnis einer anderen API über ein zurückgegebenes Promise bereit, sodass Aufrufer das Ergebnis mit Handlern oder await verarbeiten können.
Das Einbetten eines Timers erzeugt eine wiederverwendbare Pause, deren Abschluss mit dem an den aufrufenden Code zurückgegebenen Promise verbunden ist.
Promise.all
Promise.all kombiniert Eingabe-Promises zu einem Promise für alle erfolgreichen Werte in Eingabereihenfolge und wird bei der ersten Ablehnung einer Eingabe abgelehnt.
Die langsamere erste Eingabe bleibt im Ergebnisarray an erster Stelle, selbst wenn die zweite Eingabe früher endet.
Promise.allSettled
Promise.allSettled wartet auf jede Eingabe und wird mit Datensätzen in Eingabereihenfolge erfüllt, die jeden Erfüllungswert oder Ablehnungsgrund beschreiben.
Eine Seite behält einen erfolgreichen Bereich und zeigt einen Fehler für einen fehlgeschlagenen Bereich an, nachdem sie beide Ergebnisdatensätze geprüft hat.
Promise.any
Promise.any wird mit dem ersten erfolgreichen Eingabewert erfüllt und nur dann mit AggregateError abgelehnt, wenn jede Eingabe abgelehnt wurde.
Ein früher fehlgeschlagener Versuch wird ignoriert, während ein anderer Versuch noch die Chance hat, ein nutzbares Ergebnis zu liefern.
Promise.race
Promise.race folgt der ersten abgeschlossenen Eingabe und übernimmt entweder deren Erfolgswert oder deren Ablehnungsgrund als kombiniertes Ergebnis.
Ein ablehnender Timeout gewinnt gegen eine langsamere erfolgreiche Operation, während die langsame Operation weiterläuft.
Promisifizierung
Die Promisifizierung wandelt eine Callback-API in eine API um, die ein Promise zurückgibt, indem sie Erfolg und Fehler des Callbacks mit dem Abschluss des Promises verbindet.
Ein Wrapper um eine Callback-API für Beschriftungen löst mit der Beschriftung auf oder lehnt mit dem Fehler ab, sodass ein asynchroner Aufrufer auf die abgeschlossene Operation warten kann.

R

Rückfalllösung
Eine Rückfalllösung ist ein alternativer Wert oder ein alternatives Verhalten, das nach einem Fehler genutzt wird, wenn die größere Aktion ohne ihr ursprüngliches Ergebnis weiterhin sinnvolle Arbeit leisten kann.
Eine fehlende optionale Designeinstellung fällt auf ein helles Design zurück, während der Rest der Seite weiterlädt.
Run-to-Completion-Prinzip
Das Run-to-Completion-Prinzip bedeutet, dass asynchron eingereihte Callbacks den laufenden Code eines einmal gestarteten Tasks oder Microtasks nicht unterbrechen können.
Wird ein Timer mitten in einer Schleife bereit, fügt er seinen Callback nicht zwischen deren Anweisungen ein.

S

Sequenzielle Verarbeitung
Bei der sequenziellen Verarbeitung beginnt jeder asynchrone Schritt nach Abschluss des vorherigen, sodass Abhängigkeiten oder eine erforderliche Reihenfolge der Operationen erhalten bleiben.
Eine for...of-Schleife wartet auf eine Speicheroperation, bevor sie die nächste startet, und meldet den Abschluss nach dem letzten Erfolg.
Synchroner Callback
Ein synchroner Callback wird aufgerufen, bevor die empfangende Funktion zurückkehrt, sodass seine Arbeit während des aktuellen Aufrufs dieser Funktion stattfindet.
Der an map übergebene Callback verdoppelt jedes Element, bevor das vollständige Ergebnisarray zurückgegeben wird.
Synchroner Code
Synchroner Code beendet jede Anweisung, bevor er zur nächsten übergeht.
Eine lange Schleife verzögert die nächste Ausgabe, weil jeder Schleifendurchlauf ausgeführt wird, bevor die Kontrolle die Schleife verlassen kann.

T

Task
Ein Task ist eine eingeplante Arbeitseinheit, etwa ein Skript, ein Event-Handler oder ein Timer-Callback.
Der Browser beendet das aktuelle Skript, bevor er einen bereit gewordenen Timer-Callback startet.
Timeout
Ein Timeout ist ein eingeplanter einmaliger Callback, der nach einer Verzögerung ausführungsbereit wird. Die tatsächliche Ausführung hängt auch von der zeitlichen Planung ab.
Ein für 600 ms eingeplanter Hinweis kann später erscheinen, wenn der Hauptthread zu diesem Zeitpunkt noch andere Arbeit ausführt.
Timerkennung
Eine Timerkennung identifiziert einen eingeplanten Timer, damit das Programm ihn abbrechen kann.
Wird die zurückgegebene Kennung gespeichert, kann das nächste Eingabeereignis die vorherige ausstehende Aktualisierung von Vorschlägen abbrechen.
Top-Level-await
Top-Level-await ist await direkt im Rumpf eines JavaScript-Moduls, wo die Sprache es außerhalb einer async-Funktion erlaubt.
Ein Modul kann direkt auf seine Initialisierung warten, während eine normale Funktion weiterhin ihr eigenes Schlüsselwort async benötigt.
Truthy-Wert
Ein Truthy-Wert wird in einer Bedingung als wahr behandelt; ein Promise ist truthy, weil es ein Objekt ist, unabhängig von seinem späteren Ergebnis.
Ein Promise, das mit false erfüllt wird, führt dennoch in einen if-Zweig, wenn der Code das Promise-Objekt direkt prüft.
try-Block
Ein try-Block schützt Anweisungen, deren Ausnahmen die Kontrolle an sein zugehöriges catch übertragen können.
Wartet die umgebende Funktion innerhalb von try auf eine Vorbereitungsoperation, kann sie die Verarbeitung fortsetzen, wenn diese Operation später abgelehnt wird.

U

Unabhängige Operationen
Unabhängige Operationen können ohne die Ergebnisse der jeweils anderen starten, sodass sich ihre Wartezeiten überschneiden können, wenn das Programm sie gemeinsam startet.
Die Vorbereitung eines Einbands und einer Beschriftung kann sich überschneiden, wenn beide bereits alle benötigten Eingabedaten haben.
Unbehandelte Promise-Ablehnung
Eine unbehandelte Promise-Ablehnung ist die Ablehnung eines Promises, der noch ein Ablehnungshandler fehlt, wenn die Hostumgebung auf unbehandelte Ablehnungen prüft.
Eine ohne catch gestartete Hintergrundaktion kann abgelehnt werden, nachdem der aufrufende Code fortgefahren ist, sodass ihr Fehler unbehandelt bleibt.

V

Vergessenes await
Ein vergessenes await lässt ein Promise an einer Stelle stehen, an der Code dessen späteren Wert erwartet, und verursacht dadurch falsche Bedingungen oder numerische Operationen.
Die Multiplikation des von einer Mengenfunktion zurückgegebenen Promises ergibt NaN, statt dessen spätere Zahl zu multiplizieren.
Verschachtelter Timeout
Ein verschachtelter Timeout plant aus einem Timer-Callback heraus einen weiteren Timeout ein, sodass die nächste Verzögerung nach der Arbeit des aktuellen Durchlaufs beginnen kann.
Eine Fortschrittsprüfung plant ihren nächsten Timeout erst ein, nachdem ihre aktuelle synchrone Prüfung beendet ist.
Verschluckter Fehler
Ein verschluckter Fehler ist ein abgefangener Fehler, der ohne hilfreiche Meldung oder Weitergabe verborgen wird, wodurch die Diagnose der fehlgeschlagenen Aktion erschwert wird.
Ein leeres catch um eine Speicheroperation lässt die Seite schweigen, obwohl das Speichern nicht erfolgreich abgeschlossen wurde.
Vorzeitige Rückkehr
Eine vorzeitige Rückkehr verlässt eine Funktion, bevor deren verbleibende Anweisungen laufen.
Eine Rückkehr nach dem Protokollieren eines Callback-Fehlers verhindert, dass die nächste Zeile den Namen eines fehlenden Ergebnisses liest.

W

Weitergabe von Promise-Ablehnungen
Die Weitergabe von Promise-Ablehnungen trägt einen Fehler durch verbundene Promise-Glieder und überspringt Erfüllungshandler, bis ein Ablehnungshandler ihn behandelt.
Ein im zweiten Schritt ausgelöster Fehler erreicht das abschließende catch, ohne den Erfolgshandler des dritten Schritts auszuführen.
Wiederherstellung
Die Wiederherstellung wandelt einen fehlgeschlagenen Promise-Pfad in einen erfolgreichen um, indem ein Ablehnungshandler einen nutzbaren Wert oder ein erfülltes Promise zurückgibt.
Die Rückgabe einer Standardbeschriftung in catch gibt dem nächsten Erfolgshandler einen Wert, mit dem er fortfahren kann.
Wiederholungsversuch
Ein Wiederholungsversuch startet nach einem Fehler einen neuen Versuch, üblicherweise mit einer Verzögerung und einer endlichen Versuchsgrenze, wenn der Fehler vorübergehend sein könnte.
Eine Aufgabe ist beim dritten Aufruf erfolgreich, sodass eine Hilfsfunktion mit vier verfügbaren Versuchen zurückkehrt, ohne einen vierten Aufruf auszuführen.

Z

Zwischenergebnis
Ein Zwischenergebnis ist ein Wert, der während einer Abfolge entsteht und von einem späteren Schritt verwendet wird.
Eine async-Funktion speichert den Nutzer und dann die Bestellungen und hält beide lokalen Werte für die abschließende Meldung verfügbar.

Über diese Einheit

Von StudeliFehler melden

Erstelle deine eigenen Lernmaterialien

Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.

Jetzt starten