Versprechensketten eignen sich hervorragend zur Fehlerbehandlung. Wenn ein Versprechen abgelehnt wird, springt die Steuerung zum nächstgelegenen Ablehnungshandler. Das ist in der Praxis sehr praktisch.
Im folgenden Code ist beispielsweise die fetch
URL falsch (keine solche Site) und .catch
behandelt den Fehler:
fetch('https://no-such-server.blabla') // lehnt ab .then(response => Response.json()) .catch(err => warning(err)) // TypeError: Abruf fehlgeschlagen (der Text kann variieren)
Wie Sie sehen, muss der .catch
nicht sofort erfolgen. Es kann nach einem oder mehreren .then
erscheinen.
Oder vielleicht ist mit der Site alles in Ordnung, aber die Antwort ist kein gültiges JSON. Der einfachste Weg, alle Fehler abzufangen, besteht darin .catch
an das Ende der Kette anzuhängen:
fetch('https://javascript.info/article/promise-chaining/user.json') .then(response => Response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => Response.json()) .then(githubUser => new Promise((auflösen, ablehnen) => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); lösen(githubUser); }, 3000); })) .catch(error => alarm(error.message));
Normalerweise wird ein solcher .catch
überhaupt nicht ausgelöst. Wenn jedoch eines der oben genannten Versprechen abgelehnt wird (ein Netzwerkproblem oder ein ungültiger JSON-Code oder was auch immer), wird es abgefangen.
Der Code eines Promise-Executors und Promise-Handlers ist von einem „unsichtbaren try..catch
“ umgeben. Wenn eine Ausnahme auftritt, wird sie abgefangen und als Ablehnung behandelt.
Zum Beispiel dieser Code:
neues Versprechen((auflösen, ablehnen) => { throw new Error("Whoops!"); }).catch(alert); // Fehler: Hoppla!
…Funktioniert genauso:
neues Versprechen((auflösen, ablehnen) => { Reject(new Error("Whoops!")); }).catch(alert); // Fehler: Hoppla!
Der „unsichtbare try..catch
“ rund um den Executor fängt den Fehler automatisch ab und wandelt ihn in ein abgelehntes Versprechen um.
Dies geschieht nicht nur in der Executor-Funktion, sondern auch in ihren Handlern. Wenn wir einen .then
Handler throw
, bedeutet das ein abgelehntes Versprechen, sodass die Steuerung zum nächsten Fehlerhandler springt.
Hier ist ein Beispiel:
neues Versprechen((auflösen, ablehnen) => { lösen("ok"); }).then((result) => { throw new Error("Whoops!"); // lehnt das Versprechen ab }).catch(alert); // Fehler: Hoppla!
Dies geschieht bei allen Fehlern, nicht nur bei denen, die durch die throw
Anweisung verursacht werden. Zum Beispiel ein Programmierfehler:
neues Versprechen((auflösen, ablehnen) => { lösen("ok"); }).then((result) => { blabla(); // keine solche Funktion }).catch(alert); // ReferenceError: blabla ist nicht definiert
Der abschließende .catch
fängt nicht nur explizite Ablehnungen ab, sondern auch versehentliche Fehler in den oben genannten Handlern.
Wie wir bereits bemerkt haben, ähnelt .catch
am Ende der Kette try..catch
. Wir können so viele .then
Handler haben, wie wir wollen, und dann am Ende einen einzigen .catch
verwenden, um Fehler in allen zu behandeln.
In einem regulären try..catch
können wir den Fehler analysieren und ihn möglicherweise erneut auslösen, wenn er nicht behandelt werden kann. Das Gleiche ist für Versprechen möglich.
Wenn wir .catch
throw
, geht die Steuerung zum nächstgelegenen Fehlerbehandler. Und wenn wir den Fehler behandeln und normal beenden, wird mit dem nächstgelegenen erfolgreichen .then
Handler fortgefahren.
Im folgenden Beispiel behandelt .catch
den Fehler erfolgreich:
// die Ausführung: Catch -> then neues Versprechen((auflösen, ablehnen) => { throw new Error("Whoops!"); }).catch(function(error) { alarm("Der Fehler wurde behoben, normal fortfahren"); }).then(() => alarm("Nächste erfolgreiche Handlerausführung"));
Hier wird der .catch
Block normal beendet. Daher wird der nächste erfolgreiche .then
Handler aufgerufen.
Im folgenden Beispiel sehen wir die andere Situation mit .catch
. Der Handler (*)
fängt den Fehler ab und kann ihn einfach nicht verarbeiten (z. B. weiß er nur, wie er mit URIError
umgehen soll), also wirft er ihn erneut aus:
// die Ausführung: Catch -> Catch neues Versprechen((auflösen, ablehnen) => { throw new Error("Whoops!"); }).catch(function(error) { // (*) if (Fehlerinstanz von URIError) { // kümmere dich darum } anders { alarm("Dieser Fehler kann nicht behandelt werden"); Wurffehler; // Wenn dieser oder ein anderer Fehler ausgelöst wird, springt man zum nächsten Catch } }).then(function() { /* läuft hier nicht */ }).catch(error => { // (**) alarm(`Der unbekannte Fehler ist aufgetreten: ${error}`); // nichts zurückgeben => die Ausführung erfolgt wie gewohnt });
Die Ausführung springt vom ersten .catch
(*)
zum nächsten (**)
in der Kette.
Was passiert, wenn ein Fehler nicht behandelt wird? Wir haben zum Beispiel vergessen, .catch
an das Ende der Kette anzuhängen, wie hier:
neues Versprechen(Funktion() { noSuchFunction(); // Fehler hier (keine solche Funktion) }) .then(() => { // erfolgreiche Promise-Handler, einer oder mehrere }); // ohne .catch am Ende!
Im Falle eines Fehlers wird das Versprechen abgelehnt und die Ausführung sollte zum nächstgelegenen Ablehnungshandler springen. Aber es gibt keine. Der Fehler bleibt also „stecken“. Es gibt keinen Code, um damit umzugehen.
In der Praxis bedeutet dies, genau wie bei normalen, nicht behandelten Fehlern im Code, dass etwas furchtbar schief gelaufen ist.
Was passiert, wenn ein regulärer Fehler auftritt und nicht von try..catch
abgefangen wird? Das Skript bricht mit einer Meldung in der Konsole ab. Ähnliches passiert bei unbehandelten Versprechensablehnungen.
Die JavaScript-Engine verfolgt solche Ablehnungen und generiert in diesem Fall einen globalen Fehler. Sie können es in der Konsole sehen, wenn Sie das obige Beispiel ausführen.
Im Browser können wir solche Fehler mit dem Event unhandledrejection
abfangen:
window.addEventListener('unhandledrejection', function(event) { // Das Event-Objekt hat zwei besondere Eigenschaften: alarm(event.promise); // [Objektversprechen] – das Versprechen, das den Fehler generiert hat alarm(event.reason); // Fehler: Hoppla! – das nicht behandelte Fehlerobjekt }); neues Versprechen(Funktion() { throw new Error("Whoops!"); }); // kein Catch, um den Fehler zu behandeln
Das Ereignis ist Teil des HTML-Standards.
Wenn ein Fehler auftritt und kein .catch
vorhanden ist, wird der unhandledrejection
Handler ausgelöst und ruft das event
mit den Informationen zum Fehler ab, damit wir etwas unternehmen können.
Normalerweise sind solche Fehler nicht behebbar. Daher besteht unser bester Ausweg darin, den Benutzer über das Problem zu informieren und den Vorfall wahrscheinlich dem Server zu melden.
In Nicht-Browser-Umgebungen wie Node.js gibt es andere Möglichkeiten, nicht behandelte Fehler zu verfolgen.
.catch
behandelt Fehler in Versprechen aller Art: sei es ein reject()
-Aufruf oder ein Fehler, der in einem Handler ausgegeben wird.
.then
fängt auf die gleiche Weise auch Fehler ab, wenn das zweite Argument (das der Fehlerbehandler ist) angegeben wird.
Wir sollten .catch
genau an den Stellen platzieren, an denen wir mit Fehlern umgehen wollen und wissen, wie wir damit umgehen sollen. Der Handler sollte Fehler analysieren (benutzerdefinierte Fehlerklassen helfen) und unbekannte Fehler erneut auslösen (vielleicht handelt es sich um Programmierfehler).
Es ist in Ordnung, .catch
überhaupt nicht zu verwenden, wenn es keine Möglichkeit gibt, einen Fehler zu beheben.
Auf jeden Fall sollten wir über den unhandledrejection
-Ereignishandler (für Browser und Analoge für andere Umgebungen) verfügen, um nicht behandelte Fehler zu verfolgen und den Benutzer (und wahrscheinlich unseren Server) darüber zu informieren, damit unsere App nie „einfach stirbt“.
Was denken Sie? Wird der .catch
ausgelöst? Erklären Sie Ihre Antwort.
neues Versprechen(Funktion(auflösen, ablehnen) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert);
Die Antwort lautet: Nein, das wird es nicht :
neues Versprechen(Funktion(auflösen, ablehnen) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert);
Wie im Kapitel erwähnt, gibt es einen „impliziten try..catch
“ rund um den Funktionscode. Somit werden alle synchronen Fehler behandelt.
Aber hier wird der Fehler nicht während der Ausführung des Executors generiert, sondern später. Das Versprechen kann also nicht damit umgehen.