webentwicklung-frage-antwort-db.com.de

Umgang mit mehreren Fängen in einer Versprechenkette

Ich bin noch relativ neu bei Versprechungen und verwende derzeit Bluebird, allerdings habe ich ein Szenario, bei dem ich nicht ganz sicher bin, wie ich damit am besten umgehen soll.

So habe ich zum Beispiel eine Versprechungskette innerhalb 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 Konto nach Id ab
  • Wenn an diesem Punkt eine Ablehnung vorliegt, bombardieren Sie den Fehler und geben Sie einen Fehler zurück
  • Wenn kein Fehler vorliegt, 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 den Fehler und geben Sie einen anderen Fehler zurück
  • Wenn kein Fehler vorliegt, ändern Sie die Kennwörter
  • Dann kehren Sie zum Erfolg zurück
  • Wenn etwas schief gelaufen ist, geben Sie eine 500 zurück

Derzeit scheinen Fänge die Verkettung nicht aufzuhalten, und das macht Sinn. Ich frage mich, ob es einen Weg gibt, die Kette irgendwie zu zwingen, an einem bestimmten Punkt anzuhalten, basierend auf den Fehlern, oder ob es einen besseren Weg gibt um dies zu strukturieren, um eine Form des Verzweigungsverhaltens zu erhalten, da if X do Y else Z vorliegt.

Jede Hilfe wäre toll.

112
Grofit

Dieses Verhalten ist genau wie bei einem synchronen 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 von .catch - um Fehler beheben zu können. Es kann wünschenswert sein, erneut zu werfen, um zu signalisieren, dass der Status 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 jedoch nicht in Ihrem Fall, da der Fehler von einem späteren Handler abgefangen wird. Das eigentliche Problem hier ist, dass verallgemeinerte "HANDLE ANYTHING" -Fehlerbehandlungsroutinen im Allgemeinen eine schlechte Praxis sind und in anderen Programmiersprachen und Ökosystemen äußerst missbilligt werden. Aus diesem Grund bietet Bluebird getippte und Prädikatsfänge an.

Der zusätzliche Vorteil ist, 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 welche Fehler der Client erhält. Später, wenn Ihre App wächst, möchten Sie möglicherweise die Geschäftslogik (Abfragen der Datenbank und Verarbeitung Ihrer Daten) von dem, was Sie an den Client senden, trennen (Welcher http-Statuscode, welcher Text und welche Antwort).

So schreibe ich Ihren Code.

Zuerst würde ich .Query eine NoSuchAccountError werfen, ich würde sie von Promise.OperationalError subclassieren, was Bluebird bereits bietet. Wenn Sie nicht sicher sind, wie Sie einen Fehler in eine Unterklasse unterteilen, lassen Sie es mich wissen.

Ich würde es zusätzlich für AuthenticationError unterteilen und dann so etwas tun:

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

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

Nun würde ich es vom Routen-Handler als solchen nennen:

 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 ist die Logik an einem Ort und die Entscheidung, wie Fehler an den Client gehandhabt werden sollen, ist an einem Ort und sie stören sich nicht.

119

.catch funktioniert wie die try-catch-Anweisung, was bedeutet, dass Sie am Ende nur einen Haken brauchen:

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" });
            }
        });
44
Esailija

Ich frage mich, ob es eine Möglichkeit gibt, die Kette irgendwie zu zwingen, an einem bestimmten Punkt anzuhalten, basierend auf den Fehlern

Nein. Sie können eine Kette nicht wirklich "beenden", es sei denn, Sie werfen eine Ausnahme, die bis zum Ende sprudelt. Siehe Benjamin Gruenbaums Antwort , wie das geht.

Eine Ableitung seines Musters wäre, die Fehlertypen nicht zu unterscheiden, sondern Fehler zu verwenden, die statusCode- und body-Felder haben, die von einem einzigen generischen .catch-Handler gesendet werden können. Abhängig von Ihrer Anwendungsstruktur kann seine Lösung jedoch sauberer sein.

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

Ja, Sie können verzweigen mit Versprechen . 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" });
});
16
Bergi

Ich habe diesen Weg gemacht:

Am Ende verlassen Sie Ihren Fang. Und werfen Sie einfach einen Fehler, wenn dies in der Mitte Ihrer Kette geschieht.

    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 so aussehen:

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

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

Mozilla Developer Network - Versprechen verwenden

Bearbeiten: Ich habe dies eingereicht, weil es die gewünschte Funktionalität im Allgemeinen bietet. Dies ist jedoch in diesem speziellen Fall nicht der Fall. Denn wie bereits von anderen ausführlich erläutert, soll .catch den Fehler beheben. Sie können beispielsweise keine Antwort an den Client in mehrfachen .catch Rückrufen senden, da ein .catch ohne explizite return löst es in diesem Fall mit undefined auf , wodurch das Fortfahren von .then ausgelöst wird, obwohl Ihre Kette nicht wirklich aufgelöst ist, was möglicherweise zu einem folgenden .catch führt um eine weitere Antwort an den Client auszulösen und zu senden, die einen Fehler verursacht und wahrscheinlich ein UnhandledPromiseRejection auf Ihre Weise wirft. Ich hoffe, dieser verschachtelte Satz hat für Sie einen Sinn ergeben.

1
denkquer

Ich denke, die Antwort von Benjamin Gruenbaum oben ist die beste Lösung für eine komplexe Logiksequenz, aber hier ist meine Alternative für einfachere Situationen. Ich verwende einfach ein errorEncountered-Flag zusammen mit return Promise.reject(), um nachfolgende then- oder catch-Anweisungen zu überspringen. So würde es so 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 das letzte catch nur return; und nicht return Promise.reject(); enthält, da es kein nachfolgendes then gibt, das wir überspringen müssen, und dies würde als eine nicht behandelte Promise-Zurückweisung gelten, die dem Knoten nicht gefällt. Wie oben geschrieben, wird das letzte catch ein friedlich gelöstes Versprechen zurückgeben.

0

Anstelle von .then().catch()... können Sie .then(resolveFunc, rejectFunc) ausführen. Diese Versprechenkette wäre besser, wenn Sie die Dinge auf dem Weg erledigen würden. So würde ich es umschreiben:

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: Die if (error != null) ist eine Art Hack, um mit dem neuesten Fehler zu interagieren.

0
mvndaai