Manipulation de prises multiples dans la chaîne promise

Je suis encore assez nouveau pour les promesses et j'utilise bluebird actuellement, mais j'ai un scénario où je ne suis pas tout à fait sûr de la meilleure façon d'y faire face.

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 obtenir le 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é vers 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, alors bombarder et renvoyer une erreur différente
  • s'il n'y a pas d'erreur, changez les mots de passe
  • puis retour succès
  • si quelque chose d'autre a mal tourné, retournez un 500

Donc actuellement les captures ne semblent pas arrêter le chaînage, et cela a du sens, donc 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, ou s'il y a un meilleur manière de structurer ceci pour obtenir une forme de comportement de branchement, comme il y a un cas de if X do Y else Z.

Toute aide serait géniale.

106
demandé sur Grofit 2014-09-27 20:02:49

5 réponses

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 repasser 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 puisque l'erreur sera détectée par un gestionnaire ultérieur. Le vrai problème ici est que les gestionnaires d'erreurs "HANDLE ANYTHING" généralisés sont une mauvaise pratique en général et sont extrêmement mal vus dans d'autres langages de programmation et écosystème. Pour cette raison, Bluebird offre des captures typées et prédicats.

L'avantage supplémentaire est que votre logique métier ne doit pas (et ne devrait pas) être consciente du cycle de demande/réponse du tout. Il n'est pas de la responsabilité de la requête de décider quel statut HTTP et quelle erreur le client obtient et plus tard à mesure que votre application grandit, vous pouvez 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.

D'abord, j'obtiendrais .Query pour lancer un NoSuchAccountError, je le classerais à partir de Promise.OperationalError que Bluebird fournit déjà. Si vous ne savez pas comment sous-classer une erreur, faites-le moi savoir.

Je le sous-classe en plus pour AuthenticationError et ensuite faire 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 de route 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 tout en un seul endroit et la décision de gérer les erreurs pour le client est tout en un seul endroit et ils ne s'encombrent pas.

117
répondu Benjamin Gruenbaum 2014-09-27 18:06:00

.catch fonctionne comme l'instruction try-catch, ce qui signifie que vous n'avez besoin que d'une 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" });
            }
        });
41
répondu Esailija 2014-09-27 17:59:48

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 "mettre fin" à une chaîne, sauf si vous lancez une exception qui fait des bulles jusqu'à sa fin. Voir la réponse de Benjamin Gruenbaum pour savoir comment faire cela.

Une dérivation de son modèle ne serait pas de distinguer les types d'erreur, mais d'utiliser des erreurs qui ont des champs statusCode et body qui peuvent être envoyés à partir d'un seul gestionnaire .catch Générique. Selon la structure de votre application, sa solution pourrait être plus propre.

Ou s'il existe 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" à l'imbrication - 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" });
});
15
répondu Bergi 2017-05-23 11:47:29

J'ai fait de cette façon:

Vous laissez votre prise à la fin. Et il suffit de lancer une erreur quand cela arrive à mi-chemin 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);
    }                       
}
1
répondu Leo Leao 2018-04-16 01:49:07

Au Lieu de .then().catch()... vous pouvez faire .then(resolveFunc, rejectFunc). Cette chaîne de promesses serait mieux si vous manipuliez 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), c'est un peu un hack pour interagir avec l'erreur la plus récente.

0
répondu mvndaai 2018-04-30 21:43:47