Umgang mit mehreren Fängen in der Versprechenskette

125

Ich bin noch ziemlich neu in Versprechungen und benutze derzeit Bluebird, aber ich habe ein Szenario, in dem ich nicht ganz sicher bin, wie ich am besten damit umgehen soll.

So habe ich zum Beispiel eine Versprechen-Kette in einer Express-App wie folgt:

repository.Query(getAccountByIdQuery)
        .catch(function(error){
            res.status(404).send({ error: "No account found with this Id" });
        })
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .catch(function(error) {
            res.status(406).send({ OldPassword: error });
        })
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error){
            console.log(error);
            res.status(500).send({ error: "Unable to change password" });
        });

Das Verhalten, nach dem ich bin, ist also:

  • Geht, um Konto durch ID zu erhalten
  • Wenn es zu diesem Zeitpunkt eine Ablehnung gibt, bombardieren Sie und geben Sie einen Fehler zurück
  • Wenn kein Fehler auftritt, konvertieren Sie das zurückgegebene Dokument in ein Modell
  • Überprüfen Sie das Kennwort mit dem Datenbankdokument
  • Wenn die Passwörter nicht übereinstimmen, bombardieren Sie und geben Sie einen anderen Fehler zurück
  • Wenn kein Fehler vorliegt, ändern Sie die Passwörter
  • Dann kehren Sie zum Erfolg zurück
  • Wenn etwas anderes schief gelaufen ist, geben Sie eine 500 zurück

Derzeit scheinen Fänge die Verkettung nicht zu stoppen, und das macht Sinn. Ich frage mich daher, ob es für mich eine Möglichkeit gibt, die Kette aufgrund der Fehler an einem bestimmten Punkt anzuhalten, oder ob es einen besseren Weg gibt dies zu strukturieren, um irgendeine Form von Verzweigungsverhalten zu erhalten, wie es einen Fall von gibt if X do Y else Z.

Jede Hilfe wäre großartig.

Grofit
quelle
Können Sie entweder umwerfen oder vorzeitig zurückkehren?
Pieter21

Antworten:

126

Dieses Verhalten ist genau wie ein synchroner Wurf:

try{
    throw new Error();
} catch(e){
    // handle
} 
// this code will run, since you recovered from the error!

Das ist die Hälfte des Punktes .catch- um Fehler beheben zu können. Es kann wünschenswert sein, erneut zu werfen, um zu signalisieren, dass der Zustand immer noch ein Fehler ist:

try{
    throw new Error();
} catch(e){
    // handle
    throw e; // or a wrapper over e so we know it wasn't handled
} 
// this code will not run

Dies allein funktioniert in Ihrem Fall jedoch nicht, da der Fehler von einem späteren Handler abgefangen wird. Das eigentliche Problem hierbei ist, dass verallgemeinerte "HANDLE ANYTHING" -Fehlerbehandlungsroutinen im Allgemeinen eine schlechte Praxis sind und in anderen Programmiersprachen und Ökosystemen äußerst verpönt sind. Aus diesem Grund bietet Bluebird typisierte und prädikative Fänge an.

Der zusätzliche Vorteil besteht darin, dass Ihre Geschäftslogik den Anforderungs- / Antwortzyklus überhaupt nicht kennen muss (und sollte). Es liegt nicht in der Verantwortung der Abfrage, zu entscheiden, welchen HTTP-Status und Fehler der Client erhält. Später, wenn Ihre App wächst, möchten Sie möglicherweise die Geschäftslogik (wie Sie Ihre Datenbank abfragen und wie Sie Ihre Daten verarbeiten) von dem trennen, was Sie an den Client senden (Welcher http-Statuscode, welcher Text und welche Antwort).

So würde ich Ihren Code schreiben.

Zuerst würde ich .Queryeine werfen NoSuchAccountError, ich würde es unterordnen, von Promise.OperationalErrorder Bluebird bereits bereitstellt. Wenn Sie sich nicht sicher sind, wie Sie einen Fehler unterordnen sollen, lassen Sie es mich wissen.

Ich würde es zusätzlich unterordnen AuthenticationErrorund dann etwas tun wie:

function changePassword(queryDataEtc){ 
    return repository.Query(getAccountByIdQuery)
                     .then(convertDocumentToModel)
                     .then(verifyOldPassword)
                     .then(changePassword);
}

Wie Sie sehen können, ist es sehr sauber und Sie können den Text wie eine Bedienungsanleitung lesen, was dabei passiert. Es ist auch von der Anfrage / Antwort getrennt.

Jetzt würde ich es vom Routen-Handler als solches bezeichnen:

 changePassword(params)
 .catch(NoSuchAccountError, function(e){
     res.status(404).send({ error: "No account found with this Id" });
 }).catch(AuthenticationError, function(e){
     res.status(406).send({ OldPassword: error });
 }).error(function(e){ // catches any remaining operational errors
     res.status(500).send({ error: "Unable to change password" });
 }).catch(function(e){
     res.status(500).send({ error: "Unknown internal server error" });
 });

Auf diese Weise befindet sich die Logik an einem Ort und die Entscheidung, wie mit Fehlern für den Client umgegangen werden soll, ist an einem Ort und sie überlagern sich nicht.

Benjamin Gruenbaum
quelle
11
Möglicherweise möchten Sie hinzufügen, dass der Grund für die Verwendung eines Zwischenhandlers .catch(someSpecificError)für einen bestimmten Fehler darin besteht, dass Sie einen bestimmten Fehlertyp (der harmlos ist) abfangen, damit umgehen und den folgenden Ablauf fortsetzen möchten. Zum Beispiel habe ich einen Startcode, der eine Reihe von Aufgaben hat. Das erste ist, die Konfigurationsdatei von der Festplatte zu lesen, aber wenn diese Konfigurationsdatei fehlschlägt, ist dies ein OK-Fehler (das Programm hat Standardeinstellungen eingebaut), damit ich diesen spezifischen Fehler behandeln und den Rest des Ablaufs fortsetzen kann. Es kann auch besser sein, die Reinigung erst später zu verlassen.
jfriend00
1
Ich dachte, dass "Das ist die Hälfte des Punktes von .catch - um Fehler beheben zu können" dies klar machte, aber danke für die weitere Klarstellung, das ist ein gutes Beispiel.
Benjamin Gruenbaum
1
Was ist, wenn Bluebird nicht verwendet wird? Einfache es6-Versprechen enthalten nur eine Zeichenfolgenfehlermeldung, die an catch übergeben wird.
Uhrmacher
3
@clocksmith mit ES6 verspricht, dass Sie nicht instanceofweiterkommen und alles manuell erledigen können.
Benjamin Gruenbaum
1
Für diejenigen, die nach einer Referenz für die Unterklasse von Fehlerobjekten suchen, lesen Sie bluebirdjs.com/docs/api/catch.html#filtered-catch . Der Artikel gibt auch die hier gegebene Antwort auf mehrere Fänge ziemlich genau wieder.
Mummybot
47

.catchfunktioniert wie die try-catchAnweisung, was bedeutet, dass Sie am Ende nur einen Fang benötigen:

repository.Query(getAccountByIdQuery)
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error) {
            if (/*see if error is not found error*/) {
                res.status(404).send({ error: "No account found with this Id" });
            } else if (/*see if error is verification error*/) {
                res.status(406).send({ OldPassword: error });
            } else {
                console.log(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        });
Esailija
quelle
1
Ja, ich wusste davon, aber ich wollte keine große Fehlerkette machen, und es schien lesbarer, es zu tun, wenn es gebraucht wurde. Daher der Haken am Ende, aber ich mag die Idee von getippten Fehlern, da dies die Absicht aussagekräftiger macht.
Grofit
8
@Grofit für das, was es wert ist - getippte Fänge in Bluebird waren zunächst Petkas (Esailijas) Idee :) Keine Notwendigkeit, ihn davon zu überzeugen, dass sie hier ein bevorzugter Ansatz sind. Ich denke, er wollte Sie nicht verwirren, da viele Leute in JS das Konzept nicht sehr kennen.
Benjamin Gruenbaum
17

Ich frage mich, ob es für mich eine Möglichkeit gibt, die Kette aufgrund der Fehler an einem bestimmten Punkt anzuhalten

Nein. Sie können eine Kette nicht wirklich "beenden", es sei denn, Sie lösen eine Ausnahme aus, die bis zu ihrem Ende sprudelt. Siehe dazu Benjamin Gruenbaums Antwort .

Eine Ableitung seines Musters wäre nicht die Unterscheidung von Fehlertypen, sondern die Verwendung von Fehlern statusCodeund bodyFeldern, die von einem einzelnen generischen .catchHandler gesendet werden können . Abhängig von Ihrer Anwendungsstruktur ist seine Lösung möglicherweise sauberer.

oder wenn es einen besseren Weg gibt, dies zu strukturieren, um irgendeine Form von Verzweigungsverhalten zu erhalten

Ja, Sie können mit Versprechungen verzweigen . Dies bedeutet jedoch, die Kette zu verlassen und zum Verschachteln zurückzukehren, genau wie Sie es in einer verschachtelten if-else- oder try-catch-Anweisung tun würden:

repository.Query(getAccountByIdQuery)
.then(function(account) {
    return convertDocumentToModel(account)
    .then(verifyOldPassword)
    .then(function(verification) {
        return changePassword(verification)
        .then(function() {
            res.status(200).send();
        })
    }, function(verificationError) {
        res.status(406).send({ OldPassword: error });
    })
}, function(accountError){
    res.status(404).send({ error: "No account found with this Id" });
})
.catch(function(error){
    console.log(error);
    res.status(500).send({ error: "Unable to change password" });
});
Bergi
quelle
5

Ich habe so gemacht:

Sie verlassen Ihren Fang am Ende. Und werfen Sie einfach einen Fehler, wenn es in der Mitte Ihrer Kette passiert.

    repository.Query(getAccountByIdQuery)
    .then((resultOfQuery) => convertDocumentToModel(resultOfQuery)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')
    .then((model) => verifyOldPassword(model)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')        
    .then(changePassword)
    .then(function(){
        res.status(200).send();
    })
    .catch((error) => {
    if (error.name === 'no_account'){
        res.status(404).send({ error: "No account found with this Id" });

    } else  if (error.name === 'wrong_old_password'){
        res.status(406).send({ OldPassword: error });

    } else {
         res.status(500).send({ error: "Unable to change password" });

    }
});

Ihre anderen Funktionen würden wahrscheinlich ungefähr so ​​aussehen:

function convertDocumentToModel(resultOfQuery) {
    if (!resultOfQuery){
        throw new Error('no_account');
    } else {
    return new Promise(function(resolve) {
        //do stuff then resolve
        resolve(model);
    }                       
}
Leo Leao
quelle
4

Wahrscheinlich etwas spät zur Party, aber es ist möglich, .catchwie hier gezeigt zu nisten :

Mozilla Developer Network - Versprechen verwenden

Bearbeiten: Ich habe dies eingereicht, weil es die angeforderte Funktionalität im Allgemeinen bietet. In diesem speziellen Fall ist dies jedoch nicht der Fall. Denn wie bereits von anderen ausführlich erklärt, .catchsoll der Fehler behoben werden. Sie können beispielsweise keine Antwort in mehreren .catch Rückrufen an den Client senden, da dies in diesem Fall durch a .catchohne explizite return Auflösung behoben wird. Dies führt dazu undefined, dass das Verfahren .thenausgelöst wird, obwohl Ihre Kette nicht wirklich aufgelöst ist, wodurch möglicherweise eine Folge .catchausgelöst und gesendet wird eine weitere Antwort an den Client, die einen Fehler verursacht und wahrscheinlich UnhandledPromiseRejectionIhren Weg wirft . Ich hoffe, dieser verschlungene Satz hat für Sie einen Sinn ergeben.

denkquer
quelle
1
@AntonMenshov Du hast recht. Ich erweiterte meine Antwort und erklärte, warum sein gewünschtes Verhalten mit Verschachtelung immer noch nicht möglich ist
denkquer
2

Anstelle von .then().catch()...dir kann .then(resolveFunc, rejectFunc). Diese Versprechenskette wäre besser, wenn Sie die Dinge auf dem Weg erledigen würden. Hier ist, wie ich es umschreiben würde:

repository.Query(getAccountByIdQuery)
    .then(
        convertDocumentToModel,
        () => {
            res.status(404).send({ error: "No account found with this Id" });
            return Promise.reject(null)
        }
    )
    .then(
        verifyOldPassword,
        () => Promise.reject(null)
    )
    .then(
        changePassword,
        (error) => {
            if (error != null) {
                res.status(406).send({ OldPassword: error });
            }
            return Promise.Promise.reject(null);
        }
    )
    .then(
        _ => res.status(200).send(),
        error => {
            if (error != null) {
                console.error(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        }
    );

Hinweis: Dies if (error != null)ist ein Hack, um mit dem neuesten Fehler zu interagieren.

mvndaai
quelle
1

Ich denke, Benjamin Gruenbaums Antwort oben ist die beste Lösung für eine komplexe Logiksequenz, aber hier ist meine Alternative für einfachere Situationen. Ich benutze nur ein errorEncounteredFlag zusammen mit return Promise.reject(), um nachfolgende thenoder catchAnweisungen zu überspringen . So würde es aussehen:

let errorEncountered = false;
someCall({
  /* do stuff */
})
.catch({
  /* handle error from someCall*/
  errorEncountered = true;
  return Promise.reject();
})
.then({
  /* do other stuff */
  /* this is skipped if the preceding catch was triggered, due to Promise.reject */
})
.catch({
  if (errorEncountered) {
    return;
  }
  /* handle error from preceding then, if it was executed */
  /* if the preceding catch was executed, this is skipped due to the errorEncountered flag */
});

Wenn Sie mehr als zwei Then / Catch-Paare haben, sollten Sie wahrscheinlich die Lösung von Benjamin Gruenbaum verwenden. Dies funktioniert jedoch für eine einfache Einrichtung.

Beachten Sie, dass die endgültige catchnur hat return;statt return Promise.reject();, weil es keine spätere ist , thendass wir überspringen müssen, und es würde als nicht behandelte Versprechen Ablehnung zählen, die Knoten nicht wie. Wie oben geschrieben, wird das Finale catchein friedlich gelöstes Versprechen zurückgeben.

temporärer_Benutzername
quelle