Asynchrones JavaScript
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
- Unterscheide synchronen und asynchronen Code, verfolge die Ereignisschleife anhand des Aufrufstapels und ihrer Warteschlangen und plane Aufgaben mit Timern.
- Unterscheide synchronen und asynchronen CodeErklä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 EreignisschleifeVerfolge, 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 TimernFü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.
- Unterscheide synchronen und asynchronen Code
- Ü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 OperationenSchreibe 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ölleNutze 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.
- Übergib Callbacks an asynchrone Operationen
- 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 PromiseBeschreibe 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 PromisesRegistriere 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 AbfolgeGib Werte und Promises aus Handlern zurück, um flache Ketten abhängiger Schritte aufzubauen, und vermeide fehlende Rückgaben und unnötige Verschachtelung.
- Erkläre Promise-Zustände und erstelle ein Promise
- 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 awaitDeklariere 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 umWandle 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 awaitErkenne und behebe vergessene awaits, asynchrone Callbacks in forEach, als Bedingungen verwendete Promises und asynchrone Aufrufe, deren Fehler unbehandelt bleiben.
- Schreibe async-Funktionen und nutze await
- 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-KettenVerfolge, 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 awaitNutze 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 fortVerhindere 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.
- Behandle Ablehnungen in Promise-Ketten
- 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 ErgebnisseFü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.anyNutze 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.
- Starte Aufgaben gemeinsam und warte mit Promise.all auf ihre Ergebnisse
Asynchrones JavaScript einfach erklärt
1Erklä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");
- Das Skript gibt „begin“ aus.
- Es registriert einen Timer-Callback; es ruft ihn nicht auf.
- Es reiht den Handler des erfüllten Promises als Microtask ein.
- Es gibt „finish“ aus und beendet die aktuelle Arbeit.
- Der Microtask gibt „promise“ aus.
- 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.
2Schreibe 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.
3Erstelle 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.
4Schreibe 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.
5Behandle 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.
6Fü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
1Sage voraus, was jetzt und was später läuft
14 Schritte
- 1Einfü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.
- 2BeispielEinen 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.
- 3KernaussageWarten 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.
- 4HerausforderungKann 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.
- 5Erklä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.
- 6BeispielDie 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.
- 7BeispielVor 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.
- 8HerausforderungEine 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.
- 9ErklärungEinen Timer wählen und seine Kennung behalten
Nutze
setTimeoutfür einen eingeplanten Aufruf undsetIntervalfü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. - 10TippVerspä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.
- 11BeispielDie richtige Schleifenbindung nutzen
Mit
letgibt 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.
- 12HerausforderungEinen 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.
- 13HerausforderungDie 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.
- 14ZusammenfassungEine 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.
2Ein Ergebnis über einen Callback bereitstellen
9 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungDem aufrufenden Code eine nächste Aktion geben
- 2BeispielDen verzögerten Wert erzeugen
- 3VerbindungDie API steuert den Zeitpunkt
- 4HerausforderungAus welcher Funktion erfolgt die Rückkehr?
- 5BeispielEines von zwei Ergebnissen melden
- 6TippVor dem Lesen eines fehlgeschlagenen Ergebnisses stoppen
- 7HerausforderungDen aktiven Fehlerbehandlungsbereich finden
- 8HerausforderungErgebnisse und Fehler über Schritte hinweg verbinden
- 9ZusammenfassungJedes Ergebnis bis zu seiner Verarbeitung verfolgen
3Promise-Ergebnisse erstellen und verbinden
12 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungEin Objekt für ein späteres Ergebnis
- 2BeispielEine wiederverwendbare Pause erstellen
- 3KernaussageZustand und Auflösung auseinanderhalten
- 4HerausforderungEinen späteren Fehler richtig behandeln
- 5ErklärungDen richtigen Handler registrieren
- 6BeispielZwei Abonnements behalten einen Wert
- 7HerausforderungEin Ergebnis des Aufräumens lesen
- 8BeispielDas erste Glied erstellen
- 9BeispielDas nächste Glied warten lassen
- 10HerausforderungEine abgetrennte Operation reparieren
- 11HerausforderungAufräumen mit einer abhängigen Kette verbinden
- 12ZusammenfassungDen zurückgegebenen Pfad prüfen
4Eine Abfolge mit async und await umschreiben
12 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungPromises behalten, die Lesereihenfolge ändern
- 2BeispielEin async-Aufruf beginnt sofort
- 3HerausforderungDen Rückgabewert lesen
- 4BeispielEinen Callback mit einem Promise verbinden
- 5BeispielZwischenwerte im Gültigkeitsbereich halten
- 6HerausforderungPasst async allein das Ergebnis an?
- 7KernaussageDen Wert prüfen, nicht sein Promise
- 8BeispielforEach wartet nicht auf Callbacks
- 9TippEine Schleifenstrategie wählen
- 10HerausforderungEinen vorzeitigen Abschluss beheben
- 11HerausforderungAnpassung und Schleife verbinden
- 12ZusammenfassungBeide Seiten eines async-Aufrufs prüfen
5Fehler, Fortsetzung und Aufräumen verbunden halten
13 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungEntscheiden, was ein Fehler bedeuten soll
- 2BeispielDie Fortsetzung nach einem catch beobachten
- 3KernaussageAuch Protokollieren wählt einen Rückgabewert
- 4HerausforderungDas fehlgeschlagene Ergebnis erhalten
- 5BeispielDort warten, wo catch den Fehler erkennen kann
- 6TippNach lokaler Behandlung zurückkehren
- 7HerausforderungDie lokale Rückfalllösung wählen
- 8VerbindungDen Fehlerpfad der Aktion abschließen
- 9BeispielKontext und ursprüngliche Ursache bewahren
- 10BeispielEinen neuen Aufruf begrenzt wiederholen
- 11HerausforderungNach Erfolg stoppen
- 12HerausforderungFortsetzung und Aufräumen verbinden
- 13ZusammenfassungJedem Ergebnis ein Ziel geben
6Festlegen, was eine Gruppe von Aufgaben liefern muss
11 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungMit der Frage nach Abhängigkeiten beginnen
- 2BeispielBeide Operationen gemeinsam starten
- 3KernaussageEin Fehler bricht die Gruppe nicht ab
- 4HerausforderungEine Abhängigkeit beachten, den Rest überlappen lassen
- 5ErklärungJedes Ergebnis behalten, wenn Teile nützlich bleiben
- 6BeispielErsten Abschluss und ersten Erfolg unterscheiden
- 7HerausforderungFür eine Seite mit Teilergebnissen wählen
- 8TippEine Zeitüberschreitung beendet das Warten, nicht die Operation
- 9HerausforderungWas kann eine leere Gruppe abschließen?
- 10HerausforderungStartzeitpunkt und Fehlerbehandlung verbinden
- 11ZusammenfassungDie Ergebnisstrategie wählen
Asynchrones JavaScript: Übungsaufgaben
Set 1: Verfolge den Code von Aufrufen bis zu Callbacks10 Übungsfragen
Frage 1 von 10
Antwort zeigenAntwort ausblenden
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");
Antwort zeigenAntwort ausblenden
Frage 3 von 10
const reminder = setTimeout(showHint, 900); eingeplant. Welche Anweisung bricht ihn ab, bevor er läuft?Antwort zeigenAntwort ausblenden
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);
}
Antwort zeigenAntwort ausblenden
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);
Antwort zeigenAntwort ausblenden
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);
Antwort zeigenAntwort ausblenden
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);
Antwort zeigenAntwort ausblenden
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");
Antwort zeigenAntwort ausblenden
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 });
Antwort zeigenAntwort ausblenden
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");
Antwort zeigenAntwort ausblenden
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);
});
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));
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);
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);
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; }
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");
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
done(err, value) einmal auf. Was sollte ihr Promise-Wrapper tun, wenn err ein Error-Objekt ist?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");
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); });
}
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);
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
"default" zurück. Was erhält der folgende then-Handler?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));
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();
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));
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
new Error("Report failed", { cause: original }). Welche Eigenschaft hält das ursprüngliche Error-Objekt verfügbar?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
"ok" erfüllt. Was geschieht?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
await Promise.all(...) aufgerufen und beide sind erfolgreich. Wie lange dauert das gemeinsame Warten ungefähr?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Promise.all([A, B, C])?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
"backup" erfüllt. Wie unterscheiden sich race und any?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);
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
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));
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");
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));
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
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; }
}
}
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Promise.race([]), wenn niemals Eingaben hinzugefügt werden?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Promise.any([first, second]) schließlich für catch bereit?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");
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe
Schlüsselbegriffe
A
Abgelehnt
Abgeschlossen
Abhängige Operation
Ablehnungshandler
AggregateError
async-Funktion
Asynchrone Operation
Asynchroner Callback
Aufgelöst
Aufrufstapel
Auslösen einer Ausnahme
Ausstehend
await
B
Blockierung
C
Callback
Callback-Hölle
D
Debouncing
E
Eingabereihenfolge
Ereignisschleife
Erfüllt
Erfüllungshandler
Erneutes Auslösen einer Ausnahme
Error-first-Callback
Error-Objekt
Executor-Funktion
F
Fail-fast-Verhalten
Fehlende Rückgabe
Fehlerbehandlungsbereich
Fehlerereignis
Fehlerursache
finally-Block
finally-Callback
Flache Promise-Kette
Fortsetzung
H
Hauptthread
Hostumgebung
I
Ignoriertes Callback-Promise
Intervalltimer
M
Microtask
Microtask-Starvation
N
Nebenläufigkeit
P
Promise
Promise-Abonnement
Promise-Adoption
Promise-Kette
Promise-Wrapper
Promise.all
Promise.allSettled
Promise.any
Promise.race
Promisifizierung
R
Rückfalllösung
Run-to-Completion-Prinzip
S
Sequenzielle Verarbeitung
Synchroner Callback
map übergebene Callback verdoppelt jedes Element, bevor das vollständige Ergebnisarray zurückgegeben wird.Synchroner Code
T
Task
Timeout
Timerkennung
Top-Level-await
Truthy-Wert
try-Block
U
Unabhängige Operationen
Unbehandelte Promise-Ablehnung
V
Vergessenes await
Verschachtelter Timeout
Verschluckter Fehler
Vorzeitige Rückkehr
W
Weitergabe von Promise-Ablehnungen
Wiederherstellung
Wiederholungsversuch
Z
Zwischenergebnis
Über diese Einheit
Erstelle deine eigenen Lernmaterialien
Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.