Gestion de plusieurs captures dans la chaîne de promesse

125

Je suis encore assez nouveau dans les promesses et j'utilise actuellement bluebird, mais j'ai un scénario où je ne sais pas trop comment y faire face.

Donc, par exemple, j'ai une chaîne de promesses dans une application express comme ceci:

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

Donc, le comportement que je recherche est:

  • Va pour obtenir un compte par ID
  • S'il y a un rejet à ce stade, bombardez et renvoyez une erreur
  • S'il n'y a pas d'erreur, convertissez le document renvoyé en modèle
  • Vérifiez le mot de passe avec le document de base de données
  • Si les mots de passe ne correspondent pas, bombardez et renvoyez une erreur différente
  • S'il n'y a pas d'erreur, changez les mots de passe
  • Puis retourne le succès
  • Si quelque chose ne va pas, retournez un 500

Donc, actuellement, les captures ne semblent pas arrêter le chaînage, et cela a du sens, alors je me demande s'il existe un moyen pour moi de forcer d'une manière ou d'une autre la chaîne à s'arrêter à un certain point en fonction des erreurs, ou s'il existe un meilleur moyen. pour structurer cela pour obtenir une forme de comportement de branchement, comme dans le cas de if X do Y else Z.

Toute aide est la bienvenue.

Grofit
la source
Pouvez-vous renvoyer ou revenir tôt?
Pieter21

Réponses:

126

Ce comportement est exactement comme un lancer synchrone:

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

C'est la moitié de l'intérêt de .catch- être en mesure de récupérer des erreurs. Il peut être souhaitable de renvoyer pour signaler que l'état est toujours une erreur:

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

Cependant, cela seul ne fonctionnera pas dans votre cas car l'erreur sera interceptée par un gestionnaire ultérieur. Le vrai problème ici est que les gestionnaires d'erreurs généralisés «GÉRER TOUT» sont une mauvaise pratique en général et sont extrêmement mal vus dans d'autres langages de programmation et écosystèmes. Pour cette raison, Bluebird propose des captures typées et prédicat.

L'avantage supplémentaire est que votre logique métier n'a pas (et ne devrait pas) avoir à connaître du tout le cycle de demande / réponse. Il n'est pas de la responsabilité de la requête de décider quel état HTTP et quelle erreur le client obtient et plus tard, à mesure que votre application se développe, vous souhaiterez peut-être séparer la logique métier (comment interroger votre base de données et comment traiter vos données) de ce que vous envoyez au client. (quel code d'état http, quel texte et quelle réponse).

Voici comment j'écrirais votre code.

Tout d'abord, je pourrais .Querylancer un NoSuchAccountError, je le sous-classe Promise.OperationalErrorque Bluebird fournit déjà. Si vous ne savez pas comment sous-classer une erreur, faites-le moi savoir.

Je le sous-classerais également pour AuthenticationErrorpuis ferais quelque chose comme:

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

Comme vous pouvez le voir - c'est très propre et vous pouvez lire le texte comme un manuel d'instructions de ce qui se passe dans le processus. Il est également séparé de la demande / réponse.

Maintenant, je l'appellerais à partir du gestionnaire d'itinéraire en tant que tel:

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

De cette façon, la logique est au même endroit et la décision sur la manière de gérer les erreurs pour le client est au même endroit et elles ne s'encombrent pas.

Benjamin Gruenbaum
la source
11
Vous voudrez peut-être ajouter que la raison d'avoir un .catch(someSpecificError)gestionnaire intermédiaire pour une erreur spécifique est que si vous voulez attraper un type d'erreur spécifique (qui est inoffensif), traitez-le et continuez le flux qui suit. Par exemple, j'ai un code de démarrage qui a une séquence de choses à faire. La première chose est de lire le fichier de configuration à partir du disque, mais si ce fichier de configuration est manquant, c'est une erreur OK (le programme a intégré les valeurs par défaut) afin que je puisse gérer cette erreur spécifique et continuer le reste du flux. Il peut également être préférable de ne pas partir avant plus tard.
jfriend00
1
Je pensais que "C'est la moitié de l'intérêt de .catch - être capable de récupérer des erreurs" cela a été clair, mais merci de clarifier davantage, c'est un bon exemple.
Benjamin Gruenbaum
1
Et si bluebird n'est pas utilisé? Les promesses es6 simples n'ont qu'un message d'erreur de chaîne qui est passé à catch.
horloger
3
@clocksmith avec ES6 promet que vous êtes coincé à tout attraper et à faire des instanceofchceks manuellement vous-même.
Benjamin Gruenbaum
1
Pour ceux qui recherchent une référence pour le sous-classement des objets Error, lisez bluebirdjs.com/docs/api/catch.html#filtered-catch . L'article reproduit également à peu près la réponse de capture multiple donnée ici.
mummybot
47

.catchfonctionne comme l' try-catchinstruction, ce qui signifie que vous n'avez besoin que d'une seule prise à la fin:

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
la source
1
Oui, je savais cela mais je ne voulais pas faire une énorme chaîne d'erreurs, et cela semblait plus lisible en le faisant au fur et à mesure qu'il en avait besoin. D'où le catch all à la fin, mais j'aime l'idée d'erreurs typées car cela est plus descriptif quant à l'intention.
Grofit le
8
@Grofit pour ce que ça vaut - les prises tapées dans Bluebird étaient l' idée de Petka (Esailija) pour commencer :) Pas besoin de le convaincre qu'elles sont une approche préférable ici. Je pense qu'il ne voulait pas vous embrouiller car beaucoup de gens dans JS ne sont pas très conscients du concept.
Benjamin Gruenbaum
17

Je me demande s'il y a un moyen pour moi de forcer la chaîne à s'arrêter à un certain point en fonction des erreurs

Non. Vous ne pouvez pas vraiment "terminer" une chaîne, sauf si vous lancez une exception qui bouillonne jusqu'à sa fin. Voir la réponse de Benjamin Gruenbaum pour savoir comment procéder.

Une dérivation de son modèle serait de ne pas distinguer les types d'erreur, mais d'utiliser les erreurs qui ont statusCodeet les bodychamps qui peuvent être envoyés à partir d'un seul .catchgestionnaire générique . En fonction de la structure de votre application, sa solution pourrait être plus propre.

ou s'il existe une meilleure façon de structurer cela pour obtenir une forme de comportement de branchement

Oui, vous pouvez faire des branches avec des promesses . Cependant, cela signifie quitter la chaîne et "revenir" à l'imbrication - comme vous le feriez dans une instruction if-else ou try-catch imbriquée:

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
la source
5

J'ai fait de cette façon:

Vous laissez votre prise à la fin. Et lancez simplement une erreur quand cela se produit au milieu de votre chaîne.

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

    }
});

Vos autres fonctions ressembleraient probablement à ceci:

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

Probablement un peu tard à la fête, mais il est possible de nicher .catchcomme indiqué ici:

Mozilla Developer Network - Utilisation des promesses

Edit: J'ai soumis ceci car il fournit la fonctionnalité demandée en général. Cependant, ce n'est pas le cas dans ce cas particulier. Parce que, comme expliqué en détail par d'autres déjà, .catchest censé récupérer l'erreur. Vous ne pouvez pas, par exemple, envoyer une réponse au client dans plusieurs .catch rappels car un .catchsans explicite le return résout avec undefineddans ce cas, provoquant le .thendéclenchement même si votre chaîne n'est pas vraiment résolue, provoquant potentiellement le .catchdéclenchement et l'envoi d' un suivi une autre réponse au client, provoquant une erreur et vous jetant probablement un UnhandledPromiseRejectionchemin. J'espère que cette phrase alambiquée vous a donné du sens.

denkquer
la source
1
@AntonMenshov Vous avez raison. J'ai développé ma réponse, expliquant pourquoi son comportement souhaité n'est toujours pas possible avec l'imbrication
denkquer
2

Au lieu de .then().catch()...vous pouvez le faire .then(resolveFunc, rejectFunc). Cette chaîne de promesses serait meilleure si vous gérez les choses en cours de route. Voici comment je le réécrirais:

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

Remarque: le if (error != null)est un peu un hack pour interagir avec l'erreur la plus récente.

mvndaai
la source
1

Je pense que la réponse de Benjamin Gruenbaum ci - dessus est la meilleure solution pour une séquence logique complexe, mais voici mon alternative pour les situations plus simples. Je viens d' utiliser un errorEncountereddrapeau avec return Promise.reject()sauter toute modification ultérieure thenou catchdéclarations. Cela ressemblerait donc à ceci:

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 */
});

Si vous avez plus de deux paires then / catch, vous devriez probablement utiliser la solution de Benjamin Gruenbaum. Mais cela fonctionne pour une configuration simple.

Notez que la finale a catchseulement return;plutôt que return Promise.reject();, car il n'y a pas de suite thenque nous devons ignorer, et cela compterait comme un rejet de promesse non géré, ce que Node n'aime pas. Comme il est écrit ci-dessus, la finale catchrenverra une promesse résolue pacifiquement.

nom_utilisateur_temporaire
la source