web-dev-qa-db-fra.com

Gestion des captures multiples dans la chaîne de promesses

Je suis encore assez novice en ce qui concerne les promesses et j'utilise Bluebird pour le moment, mais j’ai un scénario dans lequel je ne suis pas tout à fait sûr de la meilleure façon de le gérer.

Ainsi, 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 suis après est:

  • Va chercher un compte par ID
  • S'il y a un rejet à ce stade, bombarder et renvoyer une erreur
  • S'il n'y a pas d'erreur, convertissez le document retourné à un modèle
  • Vérifier 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 d'autre a mal tourné, renvoyez 500

Les captures actuelles ne semblent donc pas arrêter la chaîne, ce qui est logique. Je me demande donc s'il est possible pour moi de forcer la chaîne à s'arrêter à un moment donné en fonction des erreurs, ou s'il existe une meilleure façon. structurer ceci pour obtenir une forme de comportement de branchement, comme il existe un cas de if X do Y else Z.

Toute aide est la bienvenue.

112
Grofit

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é du point de .catch - pour pouvoir récupérer des erreurs. Il peut être souhaitable de le rediffuser 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 "HANDLE ANYTHING" 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 par prédicats.

L'avantage supplémentaire est que votre logique métier ne doit pas (et ne devrait pas) être au courant du cycle demande/réponse. La requête n'a pas la responsabilité de décider du statut et de l'erreur HTTP obtenus par le client. Plus tard, à mesure que votre application grandit, vous souhaiterez peut-être séparer la logique métier (comment interroger votre base de données et traiter vos données) de ce que vous envoyez au client (quel code de statut http, quel texte et quelle réponse).

Voici comment j'écrirais votre code.

Premièrement, je demanderais à .Query de lancer une NoSuchAccountError; je le sous-classerais à partir de Promise.OperationalError déjà fourni par Bluebird. Si vous ne savez pas comment sous-classer une erreur, faites-le-moi savoir.

De plus, je le classerais pour AuthenticationError et ferais quelque chose comme:

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

Comme vous pouvez le constater - 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 depuis le 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 de gérer les erreurs chez le client est au même endroit et ils ne s’encombrent pas les uns des autres.

119
Benjamin Gruenbaum

.catch fonctionne comme l'instruction try-catch, ce qui signifie que vous n'avez besoin que d'une capture à 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" });
            }
        });
44
Esailija

Je me demande s’il est possible de forcer la chaîne à s’arrêter à un moment donné en se basant sur les erreurs

Non, vous ne pouvez pas vraiment "terminer" une chaîne, à moins de lever une exception qui bouillonne jusqu'à sa fin. Voir la réponse de Benjamin Gruenbaum pour savoir comment faire cela.

Une dérivation de son modèle ne consisterait pas à distinguer les types d'erreur, mais à utiliser des erreurs comportant des champs statusCode et body pouvant être envoyés à partir d'un seul gestionnaire générique .catch. En fonction de la structure de votre application, sa solution pourrait toutefois être plus propre.

ou s'il y a un meilleur moyen de structurer cela pour obtenir une forme de comportement de branchement

Oui, vous pouvez faire branchement avec des promesses . Cependant, cela signifie quitter la chaîne et "revenir en arrière" pour imbriquer - comme vous le feriez dans une instruction imbriquée if-else ou try-catch:

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

J'ai fait de cette façon:

Vous laissez votre prise à la fin. Et lancez simplement une erreur lorsque 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);
    }                       
}
3
Leo Leao

Probablement un peu tard pour la fête, mais il est possible d'imbriquer .catch comme indiqué ici:

Réseau de développeurs Mozilla - 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à, .catch est supposé récupérer l'erreur. Vous ne pouvez pas, par exemple, envoyer une réponse au client en plusieurs rappels .catch, car un .catch sans explicite return le résout avec undefined, ce qui provoque le déclenchement de .then même si votre chaîne n'est pas résolus, entraînant potentiellement le déclenchement d'un .catch suivant et l'envoi d'une autre réponse au client, générant une erreur et risquant de provoquer un UnhandledPromiseRejection votre chemin. J'espère que cette phrase compliquée a eu un sens pour vous.

1
denkquer

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 des situations plus simples. J'utilise simplement un indicateur errorEncountered avec return Promise.reject() pour ignorer toute instruction then ou catch ultérieure. Cela ressemblerait à 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 alors, vous devriez probablement utiliser la solution de Benjamin Gruenbaum. Mais cela fonctionne pour une installation simple.

Notez que la catch finale n'a que return; plutôt que return Promise.reject();, car il n'y a aucune then suivante à 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 variable finale catch renverra une promesse résolue de manière pacifique.

0

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

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: La if (error != null) est un peu un bidouillage pour interagir avec l'erreur la plus récente.

0
mvndaai