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:
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.
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.
.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" });
}
});
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" });
});
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);
}
}
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.
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.
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.