Warum kann ich nicht in einen Promise.catch-Handler werfen?

127

Warum kann ich nicht einfach einen Errorinnerhalb des catch-Rückrufs werfen und den Prozess den Fehler so behandeln lassen, als ob er in einem anderen Bereich wäre?

Wenn ich es nicht tue, wird console.log(err)nichts ausgedruckt und ich weiß nichts darüber, was passiert ist. Der Prozess endet gerade ...

Beispiel:

function do1() {
    return new Promise(function(resolve, reject) {
        throw new Error('do1');
        setTimeout(resolve, 1000)
    });
}

function do2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(new Error('do2'));
        }, 1000)
    });
}

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // This does nothing
});

Wenn Rückrufe im Haupt-Thread ausgeführt werden, warum wird der Errorvon einem Schwarzen Loch verschluckt?

demian85
quelle
11
Es wird nicht von einem schwarzen Loch verschluckt. Es lehnt das zurückgegebene Versprechen ab .catch(…).
Bergi
stattdessen .catch((e) => { throw new Error() })schreiben .catch((e) => { return Promise.reject(new Error()) })oder einfach.catch((e) => Promise.reject(new Error()))
chharvey
1
@chharvey Alle Codefragmente in Ihrem Kommentar haben genau das gleiche Verhalten, außer dass das erste offensichtlich am klarsten ist.
24ергей Гринько

Antworten:

157

Wie andere erklärt haben, liegt das "Schwarze Loch" daran, dass das Werfen in eine .catchKette mit einem abgelehnten Versprechen fortgesetzt wird und Sie keine Fänge mehr haben, was zu einer nicht abgeschlossenen Kette führt, die Fehler verschluckt (schlecht!)

Fügen Sie noch einen Haken hinzu, um zu sehen, was passiert:

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // Where does this go?
}).catch(function(err) {
    console.log(err.stack); // It goes here!
});

Ein Fang in der Mitte einer Kette ist nützlich, wenn Sie möchten, dass die Kette trotz eines fehlgeschlagenen Schritts fortgesetzt wird. Ein erneuter Wurf ist jedoch hilfreich, um weiterhin fehlzuschlagen, nachdem Sie beispielsweise Informationen protokolliert oder Bereinigungsschritte ausgeführt haben und möglicherweise sogar den Fehler geändert haben ist geworfen.

Trick

Um den Fehler wie ursprünglich beabsichtigt als Fehler in der Webkonsole anzuzeigen, verwende ich diesen Trick:

.catch(function(err) { setTimeout(function() { throw err; }); });

Sogar die Zeilennummern sind erhalten, sodass der Link in der Webkonsole mich direkt zu der Datei und Zeile führt, in der der (ursprüngliche) Fehler aufgetreten ist.

Warum es funktioniert

Jede Ausnahme in einer Funktion, die als Handler für die Erfüllung oder Ablehnung von Versprechen bezeichnet wird, wird automatisch in eine Ablehnung des Versprechens umgewandelt, das Sie zurückgeben sollen. Der Versprechen-Code, der Ihre Funktion aufruft, sorgt dafür.

Eine von setTimeout aufgerufene Funktion wird dagegen immer im stabilen JavaScript-Zustand ausgeführt, dh sie wird in einem neuen Zyklus in der JavaScript-Ereignisschleife ausgeführt. Ausnahmen dort werden von nichts erfasst und erreichen die Webkonsole. Da erralle Informationen über den Fehler enthalten sind, einschließlich des ursprünglichen Stapels, der Datei und der Zeilennummer, wird dieser weiterhin korrekt gemeldet.

Ausleger
quelle
3
Fock, das ist ein interessanter Trick. Können Sie mir helfen zu verstehen, warum das funktioniert?
Brian Keith
8
Über diesen Trick: Sie werfen, weil Sie sich anmelden möchten. Warum also nicht einfach direkt anmelden? Dieser Trick wird zu einem 'zufälligen' Zeitpunkt einen unauffindbaren Fehler auslösen ... Aber die ganze Idee von Ausnahmen (und die Art und Weise, wie Versprechen damit umgehen) besteht darin, es in die Verantwortung des Anrufers zu legen , den Fehler zu erkennen und damit umzugehen. Dieser Code macht es dem Anrufer effektiv unmöglich, mit den Fehlern umzugehen. Warum nicht einfach eine Funktion erstellen, um sie für Sie zu erledigen? function logErrors(e){console.error(e)}dann benutze es gerne do1().then(do2).catch(logErrors). Antwort selbst ist übrigens großartig, +1
Stijn de Witt
3
@jib Ich schreibe ein AWS-Lambda, das viele Versprechen enthält, die mehr oder weniger wie in diesem Fall verbunden sind. Um AWS-Alarme und -Benachrichtigungen im Fehlerfall auszunutzen, muss der Lambda-Absturz einen Fehler auslösen (denke ich). Ist der Trick der einzige Weg, dies zu erreichen?
Masciugo
2
@StijndeWitt In meinem Fall habe ich versucht, Fehlerdetails im window.onerrorEreignishandler an meinen Server zu senden . Nur mit diesem setTimeoutTrick kann dies erreicht werden. Andernfalls window.onerrorwird nie etwas über die Fehler in Promise gehört.
Hudidit
1
@hudidit Trotzdem, ob es ist console.logoder postErrorToServer, können Sie einfach das tun, was getan werden muss. Es gibt keinen Grund, warum der Code window.onerrornicht in eine separate Funktion zerlegt und von zwei Stellen aus aufgerufen werden kann. Es ist wahrscheinlich noch kürzer als die setTimeoutLinie.
Stijn de Witt
46

Wichtige Dinge, die Sie hier verstehen sollten

  1. Sowohl die thenals auch die catchFunktionen geben neue Versprechungsobjekte zurück.

  2. Durch Werfen oder explizites Ablehnen wird das aktuelle Versprechen in den abgelehnten Zustand versetzt.

  3. Da thenund catchneue Versprechen Objekte zurückgeben, können sie verkettet werden.

  4. Wenn Sie einen Versprechen-Handler ( thenoder catch) werfen oder ablehnen , wird dies im nächsten Ablehnungs-Handler auf dem Verkettungspfad behandelt.

  5. Wie von jfriend00 erwähnt, werden die thenund -Handler catchnicht synchron ausgeführt. Wenn ein Handler wirft, endet er sofort. Der Stapel wird also abgewickelt und die Ausnahme geht verloren. Aus diesem Grund lehnt das Auslösen einer Ausnahme das aktuelle Versprechen ab.


In Ihrem Fall lehnen Sie innen ab, do1indem Sie ein ErrorObjekt werfen . Jetzt befindet sich das aktuelle Versprechen im abgelehnten Zustand und die Kontrolle wird an den nächsten Handler übertragen, was thenin unserem Fall der Fall ist.

Da der thenHandler keinen Ablehnungshandler hat, do2wird der überhaupt nicht ausgeführt. Sie können dies bestätigen, indem Sie console.loges verwenden. Da das aktuelle Versprechen keinen Ablehnungshandler hat, wird es auch mit dem Ablehnungswert aus dem vorherigen Versprechen abgelehnt und die Kontrolle wird an den nächsten Handler übertragen catch.

Wie catchbei einem Ablehnungshandler console.log(err.stack);können Sie den Fehlerstapel-Trace sehen , wenn Sie sich darin befinden. Jetzt werfen Sie ein ErrorObjekt daraus, sodass das von zurückgegebene Versprechen catchebenfalls abgelehnt wird.

Da Sie dem keinen Ablehnungshandler zugeordnet haben catch, können Sie die Ablehnung nicht beobachten.


Sie können die Kette teilen und dies besser verstehen

var promise = do1().then(do2);

var promise1 = promise.catch(function (err) {
    console.log("Promise", promise);
    throw err;
});

promise1.catch(function (err) {
    console.log("Promise1", promise1);
});

Die Ausgabe, die Sie erhalten, ist ungefähr so

Promise Promise { <rejected> [Error: do1] }
Promise1 Promise { <rejected> [Error: do1] }

Innerhalb des catchHandlers 1 erhalten Sie den Wert des promiseObjekts als abgelehnt.

Ebenso wird das vom catchHandler 1 zurückgegebene Versprechen mit demselben Fehler abgelehnt, mit dem das promiseabgelehnt wurde, und wir beobachten es im zweiten catchHandler.

thefourtheye
quelle
3
Es könnte sich auch lohnen, hinzuzufügen, dass .then()Handler asynchron sind (der Stapel wird abgewickelt, bevor sie ausgeführt werden), sodass Ausnahmen in ihnen in Ablehnungen umgewandelt werden müssen, da es sonst keine Ausnahmebehandler gibt, die sie abfangen können.
jfriend00
7

Ich habe die oben beschriebene setTimeout()Methode ausprobiert ...

.catch(function(err) { setTimeout(function() { throw err; }); });

Ärgerlicherweise fand ich das völlig unprüfbar. Da es einen asynchronen Fehler auslöst, können Sie ihn nicht in eine try/catchAnweisung einschließen, da der catchWille zum Zeitpunkt des Auslösens des Fehlers nicht mehr zuhört.

Ich habe wieder nur einen Listener verwendet, der perfekt funktioniert hat und der, da JavaScript so verwendet werden soll, sehr gut testbar war.

return new Promise((resolve, reject) => {
    reject("err");
}).catch(err => {
    this.emit("uncaughtException", err);

    /* Throw so the promise is still rejected for testing */
    throw err;
});
RiggerTheGeek
quelle
3
Jest hat Timer-Mocks , die mit dieser Situation umgehen sollten.
Jordanbtucker
2

Gemäß der Spezifikation (siehe 3.III.d) :

d. Wenn der Aufruf dann eine Ausnahme auslöst, e,
  a. Wenn auflösenPromise oder ablehnenPromise aufgerufen wurde, ignorieren Sie es.
  b. Andernfalls lehnen Sie das Versprechen mit e als Grund ab.

Das heißt, wenn Sie eine Ausnahme in die thenFunktion werfen , wird diese abgefangen und Ihr Versprechen wird abgelehnt. catchmacht hier keinen Sinn, es ist nur eine Abkürzung zu.then(null, function() {})

Ich denke, Sie möchten unbehandelte Ablehnungen in Ihrem Code protokollieren. Die meisten Versprechungsbibliotheken feuern ein unhandledRejectiondafür. Hier ist ein relevanter Kern mit einer Diskussion darüber.

Just-Boris
quelle
Es ist erwähnenswert, dass der unhandledRejectionHook für serverseitiges JavaScript ist. Auf der Clientseite haben verschiedene Browser unterschiedliche Lösungen. Wir haben es noch nicht standardisiert, aber es kommt langsam aber sicher an.
Benjamin Gruenbaum
1

Ich weiß, dass dies etwas spät ist, aber ich bin auf diesen Thread gestoßen, und keine der Lösungen war für mich einfach zu implementieren, daher habe ich mir eine eigene ausgedacht:

Ich habe eine kleine Hilfsfunktion hinzugefügt, die ein Versprechen zurückgibt, wie folgt:

function throw_promise_error (error) {
 return new Promise(function (resolve, reject){
  reject(error)
 })
}

Wenn ich dann eine bestimmte Stelle in einer meiner Versprechensketten habe, an der ich einen Fehler auslösen (und das Versprechen ablehnen) möchte, kehre ich einfach mit meinem konstruierten Fehler von der obigen Funktion zurück:

}).then(function (input) {
 if (input === null) {
  let err = {code: 400, reason: 'input provided is null'}
  return throw_promise_error(err)
 } else {
  return noterrorpromise...
 }
}).then(...).catch(function (error) {
 res.status(error.code).send(error.reason);
})

Auf diese Weise habe ich die Kontrolle darüber, zusätzliche Fehler aus der Versprechenskette herauszuwerfen. Wenn Sie auch "normale" Versprechungsfehler behandeln möchten, erweitern Sie Ihren Fang, um die "selbst geworfenen" Fehler separat zu behandeln.

Hoffe das hilft, es ist meine erste Stackoverflow Antwort!

Nuudles
quelle
Promise.reject(error)statt new Promise(function (resolve, reject){ reject(error) })(was sowieso eine return-Anweisung benötigen würde)
Funkodebat
0

Ja verspricht Schluckfehler, und Sie können sie nur abfangen .catch, wie in anderen Antworten ausführlicher erläutert. Wenn Sie sich in Node.js befinden und das normale throwVerhalten reproduzieren möchten, können Sie den Stack-Trace auf die Konsole drucken und den Prozess beenden

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});
Jesús Carrera
quelle
1
Nein, das reicht nicht aus, da Sie das am Ende jeder Versprechen-Kette setzen müssten, die Sie haben. Eher an der unhandledRejectionVeranstaltung teilnehmen
Bergi
Ja, das setzt voraus, dass Sie Ihre Versprechen verketten, damit der Ausgang die letzte Funktion ist und danach nicht mehr abgefangen wird. Das Ereignis, das Sie erwähnen, ist meiner Meinung nach nur bei Verwendung von Bluebird möglich.
Jesús Carrera
Bluebird, Q, wenn, einheimische Versprechen, ... Es wird wahrscheinlich ein Standard werden.
Bergi