Briser la chaîne de promesse et appeler une fonction en fonction de l'étape de la chaîne où elle est rompue (rejetée)

135

Mettre à jour:

Pour aider les futurs téléspectateurs de cet article, j'ai créé cette démo de la réponse de pluma .

Question:

Mon objectif semble assez simple.

  step(1)
  .then(function() {
    return step(2);
  }, function() {
    stepError(1);
    return $q.reject();
  })
  .then(function() {

  }, function() {
    stepError(2);
  });

  function step(n) {
    var deferred = $q.defer();
    //fail on step 1
    (n === 1) ? deferred.reject() : deferred.resolve();
    return deferred.promise;
  }
  function stepError(n) {
    console.log(n); 
  }

Le problème ici est que si j'échoue à l'étape 1, les deux stepError(1)AND stepError(2)sont déclenchés. Si je ne return $q.rejectpuis stepError(2)ne sera pas tiré, mais step(2)sera, ce que je comprends. J'ai tout accompli sauf ce que j'essaye de faire.

Comment écrire des promesses pour pouvoir appeler une fonction en cas de rejet, sans appeler toutes les fonctions de la chaîne d'erreur? Ou y a-t-il une autre façon d'accomplir cela?

Voici une démo en direct pour que vous puissiez travailler avec quelque chose.

Mettre à jour:

J'ai en quelque sorte résolu le problème. Ici, j'attrape l'erreur à la fin de la chaîne et reject(data)je transmets les données à afin de savoir quel problème gérer dans la fonction d'erreur. Cela ne répond pas à mes besoins car je ne veux pas dépendre des données. Ce serait nul, mais dans mon cas, il serait plus propre de transmettre un rappel d'erreur à la fonction plutôt que de dépendre des données renvoyées pour déterminer ce qu'il faut faire.

Démo en direct ici (cliquez).

step(1)
  .then(function() {
    return step(2);
  })
  .then(function() {
    return step(3);
  })
  .then(false, 
    function(x) {
      stepError(x);
    }
  );
  function step(n) {
    console.log('Step '+n);
    var deferred = $q.defer();
    (n === 1) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
  }
  function stepError(n) {
    console.log('Error '+n); 
  }
m59
la source
1
Il existe une bibliothèque javascript asynchrone qui pourrait aider si cela devient plus compliqué
lucuma
Promise.prototype.catch()Les exemples sur MDN montrent la solution pour les mêmes problèmes.
toraritte le

Réponses:

199

La raison pour laquelle votre code ne fonctionne pas comme prévu est qu'il fait en fait quelque chose de différent de ce que vous pensez qu'il fait.

Disons que vous avez quelque chose comme ce qui suit:

stepOne()
.then(stepTwo, handleErrorOne)
.then(stepThree, handleErrorTwo)
.then(null, handleErrorThree);

Pour mieux comprendre ce qui se passe, supposons qu'il s'agit d'un code synchrone avec try/ catchblocks:

try {
    try {
        try {
            var a = stepOne();
        } catch(e1) {
            a = handleErrorOne(e1);
        }
        var b = stepTwo(a);
    } catch(e2) {
        b = handleErrorTwo(e2);
    }
    var c = stepThree(b);
} catch(e3) {
    c = handleErrorThree(e3);
}

Le onRejectedgestionnaire (le deuxième argument de then) est essentiellement un mécanisme de correction d'erreur (comme un catchbloc). Si une erreur est renvoyée handleErrorOne, elle sera interceptée par le prochain bloc catch ( catch(e2)), et ainsi de suite.

Ce n'est évidemment pas ce que vous vouliez.

Disons que nous voulons que toute la chaîne de résolution échoue, peu importe ce qui ne va pas:

stepOne()
.then(function(a) {
    return stepTwo(a).then(null, handleErrorTwo);
}, handleErrorOne)
.then(function(b) {
    return stepThree(b).then(null, handleErrorThree);
});

Remarque: nous pouvons laisser le handleErrorOneoù il est, car il ne sera invoqué qu'en cas de stepOnerejet (c'est la première fonction de la chaîne, donc nous savons que si la chaîne est rejetée à ce stade, cela ne peut être qu'à cause de la promesse de cette fonction) .

Le changement important est que les gestionnaires d'erreurs pour les autres fonctions ne font pas partie de la chaîne de promesse principale. Au lieu de cela, chaque étape a sa propre «sous-chaîne» avec une onRejectedqui n'est appelée que si l'étape a été rejetée (mais ne peut pas être atteinte directement par la chaîne principale).

La raison pour laquelle cela fonctionne est que les deux onFulfilledet onRejectedsont des arguments facultatifs de la thenméthode. Si une promesse est remplie (c'est-à-dire résolue) et que le suivant thendans la chaîne n'a pas de onFulfilledgestionnaire, la chaîne continuera jusqu'à ce qu'il y en ait un avec un tel gestionnaire.

Cela signifie que les deux lignes suivantes sont équivalentes:

stepOne().then(stepTwo, handleErrorOne)
stepOne().then(null, handleErrorOne).then(stepTwo)

Mais la ligne suivante n'est pas équivalente aux deux ci-dessus:

stepOne().then(stepTwo).then(null, handleErrorOne)

La bibliothèque de promesses d'Angular $qest basée sur la Qbibliothèque de kriskowal (qui a une API plus riche, mais contient tout ce que vous pouvez trouver $q). La documentation de l'API de Q sur GitHub pourrait s'avérer utile. Q implémente la spécification Promises / A + , qui explique en détail comment thenet le comportement de résolution des promesses fonctionne exactement.

ÉDITER:

Gardez également à l'esprit que si vous souhaitez sortir de la chaîne dans votre gestionnaire d'erreurs, il doit renvoyer une promesse rejetée ou lancer une erreur (qui sera automatiquement interceptée et encapsulée dans une promesse rejetée). Si vous ne renvoyez pas de promesse, thenencapsule la valeur de retour dans une promesse de résolution pour vous.

Cela signifie que si vous ne retournez rien, vous renvoyez effectivement une promesse résolue pour la valeur undefined.

Alan Plum
la source
138
Cette partie est en or: if you don't return anything, you are effectively returning a resolved promise for the value undefined.Merci @pluma
Valerio
7
C'est en effet. Je le
modifie
le rejet quitte-t-il la fonction actuelle? par exemple, la résolution ne sera pas appelée si le rejet est appelé 1er `if (mauvais) {rejet (statut); } résoudre (résultats); `
SuperUberDuper
stepOne().then(stepTwo, handleErrorOne) `stepOne (). then (null, handleErrorOne) .then (stepTwo)` Sont-ils vraiment équivalents? Je pense qu'en cas de rejet, stepOnela deuxième ligne de code s'exécutera stepTwomais la première ne s'exécutera handleErrorOneet s'arrêtera. Ou est-ce que je manque quelque chose?
JeFf
5
Ne fournit pas vraiment de solution claire à la question posée, bonne explication quand même
Yerken
57

Un peu tard à la fête mais cette solution simple a fonctionné pour moi:

function chainError(err) {
  return Promise.reject(err)
};

stepOne()
.then(stepTwo, chainError)
.then(stepThreee, chainError);

Cela vous permet de casser de la chaîne.

Vinnyq12
la source
1
M'a aidé mais pour info, vous pouvez le retourner dans le temps pour éclater dans la prise comme:.then(user => { if (user) return Promise.reject('The email address already exists.') })
Craig van Tonder
1
@CraigvanTonder, vous pouvez simplement lancer une promesse et cela fonctionnera de la même manière que le vôtre:.then(user => { if (user) throw 'The email address already exists.' })
Francisco Presencia
1
C'est la seule bonne réponse. Sinon, l'étape 3 exécutera toujours même l'étape 1 a une erreur.
wdetac
1
Juste pour clarifier, si une erreur se produit dans stepOne (), alors les deux chainError sont invoqués, non? Si cela est souhaitable. J'ai un extrait qui fait cela, je ne sais pas si j'ai mal compris quelque chose - runkit.com/embed/9q2q3rjxdar9
user320550
10

Vous avez besoin d'une .then()chaîne répétitive avec un cas spécial pour commencer et un cas spécial pour terminer.

Le truc est de faire passer le numéro de l'étape du cas d'échec à un gestionnaire d'erreur final.

  • Début: appelez step(1)sans condition.
  • Motif répétitif: enchaînez un .then()avec les rappels suivants:
    • succès: étape d'appel (n + 1)
    • échec: renvoie la valeur avec laquelle le différé précédent a été rejeté ou renvoie l'erreur.
  • Terminer: chaîner un .then()sans gestionnaire de succès et un gestionnaire d'erreur final.

Vous pouvez écrire le tout à la main, mais il est plus facile de démontrer le modèle avec des fonctions nommées et généralisées:

function nextStep(n) {
    return step(n + 1);
}

function step(n) {
    console.log('step ' + n);
    var deferred = $q.defer();
    (n === 3) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
}

function stepError(n) {
    throw(n);
}

function finalError(n) {
    console.log('finalError ' + n);
}
step(1)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(null, finalError);});

voir la démo

Notez comment dans step(), le différé est rejeté ou résolu avec n, rendant ainsi cette valeur disponible pour les rappels du suivant .then()dans la chaîne. Une fois stepErrorappelée, l'erreur est renvoyée à plusieurs reprises jusqu'à ce qu'elle soit traitée par finalError.

Betterave-Betterave
la source
Réponse informative donc ça vaut la peine de la garder, mais ce n'est pas le problème auquel je suis confronté. Je mentionne cette solution dans mon post et ce n'est pas ce que je recherche. Voir la démo en haut de mon message.
m59 du
1
m59, c'est une réponse à la question posée, "comment écrire des promesses pour pouvoir appeler une fonction en cas de rejet, sans appeler toutes les fonctions de la chaîne d'erreur?" et le titre de la question, "Briser la chaîne de promesses et appeler une fonction basée sur l'étape de la chaîne où elle est rompue (rejetée)"
Beetroot-Beetroot
Bon, comme je l'ai dit, c'est informatif et j'ai même inclus cette solution dans mon post (avec moins de détails). Cette approche vise à réparer les choses pour que la chaîne puisse continuer. Bien que cela puisse accomplir ce que je recherche, ce n'est pas aussi naturel que l'approche de la réponse acceptée. En d'autres termes, si vous voulez faire ce qui est exprimé par le titre et la question posée, adoptez l'approche de pluma.
m59
7

Lors du rejet, vous devez transmettre une erreur de rejet, puis envelopper les gestionnaires d’erreurs d’étape dans une fonction qui vérifie si le rejet doit être traité ou «relancé» jusqu’à la fin de la chaîne:

// function mocking steps
function step(i) {
    i++;
    console.log('step', i);
    return q.resolve(i);
}

// function mocking a failing step
function failingStep(i) {
    i++;
    console.log('step '+ i + ' (will fail)');
    var e = new Error('Failed on step ' + i);
    e.step = i;
    return q.reject(e);
}

// error handler
function handleError(e){
    if (error.breakChain) {
        // handleError has already been called on this error
        // (see code bellow)
        log('errorHandler: skip handling');
        return q.reject(error);
    }
    // firs time this error is past to the handler
    console.error('errorHandler: caught error ' + error.message);
    // process the error 
    // ...
    //
    error.breakChain = true;
    return q.reject(error);
}

// run the steps, will fail on step 4
// and not run step 5 and 6
// note that handleError of step 5 will be called
// but since we use that error.breakChain boolean
// no processing will happen and the error will
// continue through the rejection path until done(,)

  step(0) // 1
  .catch(handleError)
  .then(step) // 2
  .catch(handleError)
  .then(step) // 3
  .catch(handleError)
  .then(failingStep)  // 4 fail
  .catch(handleError)
  .then(step) // 5
  .catch(handleError)
  .then(step) // 6
  .catch(handleError)
  .done(function(){
      log('success arguments', arguments);
  }, function (error) {
      log('Done, chain broke at step ' + error.step);
  });

Ce que vous verriez sur la console:

step 1
step 2
step 3
step 4 (will fail)
errorHandler: caught error 'Failed on step 4'
errorHandler: skip handling
errorHandler: skip handling
Done, chain broke at step 4

Voici un code de travail https://jsfiddle.net/8hzg5s7m/3/

Si vous avez une gestion spécifique pour chaque étape, votre wrapper pourrait être quelque chose comme:

/*
 * simple wrapper to check if rejection
 * has already been handled
 * @param function real error handler
 */
function createHandler(realHandler) {
    return function(error) {
        if (error.breakChain) {
            return q.reject(error);
        }
        realHandler(error);
        error.breakChain = true;
        return q.reject(error);    
    }
}

puis ta chaîne

step1()
.catch(createHandler(handleError1Fn))
.then(step2)
.catch(createHandler(handleError2Fn))
.then(step3)
.catch(createHandler(handleError3Fn))
.done(function(){
    log('success');
}, function (error) {
    log('Done, chain broke at step ' + error.step);
});
Redben
la source
2

Si je comprends bien, vous ne voulez que l'erreur de l'étape qui a échoué, non?

Cela devrait être aussi simple que de changer le cas d'échec de la première promesse en ceci:

step(1).then(function (response) {
    step(2);
}, function (response) {
    stepError(1);
    return response;
}).then( ... )

En retournant $q.reject()dans le cas d'échec de la première étape, vous rejetez cette promesse, ce qui provoque l'appel de errorCallback dans la 2ème then(...).

Zajn
la source
Que diable ... c'est exactement ce que j'ai fait! Voyez dans mon article que j'ai essayé cela, mais la chaîne revenait et courait step(2). Maintenant, je viens de réessayer, cela ne se produit pas. Je suis tellement confus.
m59
1
J'ai vu que vous en aviez parlé. C'est bizarre cependant. Cette fonction qui contient return step(2);ne doit être appelée que lorsqu'elle est step(1)résolue avec succès.
Zajn
Grattez cela - cela se passe définitivement. Comme je l'ai dit dans mon article, si vous ne l'utilisez pas return $q.reject(), la chaîne va continuer. Dans ce cas, tout return responsegâché. Voir ceci: jsbin.com/EpaZIsIp/6/edit
m59
Hmm d'accord. Cela semble fonctionner dans le jsbin que vous avez posté lorsque j'ai changé cela, mais j'ai dû manquer quelque chose.
Zajn
Ouais, je vois vraiment que ça ne marche pas maintenant. De retour à la planche à dessin pour moi!
Zajn
2
var s = 1;
start()
.then(function(){
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/20/edit

Ou automatisé pour n'importe quel nombre d'étapes:

var promise = start();
var s = 1;
var l = 3;
while(l--) {
    promise = promise.then(function() {
        return step(s++);
    });
}
promise.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/21/edit

Esailija
la source
Mais si j'appelle, deferred.reject(n)je reçois un avertissement que la promesse est rejetée avec un objet
nonError
2

Essayez d'utiliser ceci comme libs:

https://www.npmjs.com/package/promise-chain-break

    db.getData()
.then(pb((data) => {
    if (!data.someCheck()) {
        tellSomeone();

        // All other '.then' calls will be skiped
        return pb.BREAK;
    }
}))
.then(pb(() => {
}))
.then(pb(() => {
}))
.catch((error) => {
    console.error(error);
});
Léonide
la source
2

Si vous souhaitez résoudre ce problème en utilisant async / await:

(async function(){    
    try {        
        const response1, response2, response3
        response1 = await promise1()

        if(response1){
            response2 = await promise2()
        }
        if(response2){
            response3 = await promise3()
        }
        return [response1, response2, response3]
    } catch (error) {
        return []
    }

})()
Luispa
la source
1

Attachez les gestionnaires d'erreurs en tant qu'éléments de chaîne séparés directement à l'exécution des étapes:

        // Handle errors for step(1)
step(1).then(null, function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).then(null, function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).then(null, function() { stepError(3); return $q.reject(); });
});

ou en utilisant catch():

       // Handle errors for step(1)
step(1).catch(function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).catch(function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).catch(function() { stepError(3); return $q.reject(); });
});

Remarque: Il s'agit essentiellement du même schéma que celui suggéré par Pluma dans sa réponse, mais en utilisant la dénomination du PO.

Allumeur
la source
1

Trouvé des Promise.prototype.catch()exemples sur MDN ci-dessous très utiles.

(La réponse acceptée mentionne then(null, onErrorHandler)ce qui est fondamentalement la même que catch(onErrorHandler).)

Utilisation et chaînage de la méthode catch

var p1 = new Promise(function(resolve, reject) {
  resolve('Success');
});

p1.then(function(value) {
  console.log(value); // "Success!"
  throw 'oh, no!';
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

// The following behaves the same as above
p1.then(function(value) {
  console.log(value); // "Success!"
  return Promise.reject('oh, no!');
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

Gotchas lors du lancement d'erreurs

// Throwing an error will call the catch method most of the time
var p1 = new Promise(function(resolve, reject) {
  throw 'Uh-oh!';
});

p1.catch(function(e) {
  console.log(e); // "Uh-oh!"
});

// Errors thrown inside asynchronous functions will act like uncaught errors
var p2 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    throw 'Uncaught Exception!';
  }, 1000);
});

p2.catch(function(e) {
  console.log(e); // This is never called
});

// Errors thrown after resolve is called will be silenced
var p3 = new Promise(function(resolve, reject) {
  resolve();
  throw 'Silenced Exception!';
});

p3.catch(function(e) {
   console.log(e); // This is never called
});

S'il est résolu

//Create a promise which would not call onReject
var p1 = Promise.resolve("calling next");

var p2 = p1.catch(function (reason) {
    //This is never called
    console.log("catch p1!");
    console.log(reason);
});

p2.then(function (value) {
    console.log("next promise's onFulfilled"); /* next promise's onFulfilled */
    console.log(value); /* calling next */
}, function (reason) {
    console.log("next promise's onRejected");
    console.log(reason);
});
Toraritte
la source
1

La meilleure solution est de refactoriser votre chaîne de promesses pour utiliser ES6 await. Ensuite, vous pouvez simplement revenir de la fonction pour ignorer le reste du comportement.

Je me suis cogné la tête contre ce modèle depuis plus d'un an et utiliser Wait's est le paradis.

Pete Alvin
la source
Lorsque vous utilisez pure IE async / await n'est pas pris en charge.
ndee
0

Utiliser un module SequentialPromise

Intention

Fournir un module dont la responsabilité est d'exécuter les requêtes de manière séquentielle, tout en suivant l'index actuel de chaque opération de manière ordinale. Définissez l'opération dans un modèle de commande pour plus de flexibilité.

Les participants

  • Contexte : objet dont la méthode membre effectue une opération.
  • SequentialPromise : définit une executeméthode pour enchaîner et suivre chaque opération. SequentialPromise renvoie une Promise-Chain à partir de toutes les opérations effectuées.
  • Invoker : crée une instance SequentialPromise, en lui fournissant le contexte et l'action, et appelle sa executeméthode tout en passant une liste ordinale d'options pour chaque opération.

Conséquences

Utilisez SequentialPromise lorsque le comportement ordinal de la résolution Promise est nécessaire. SequentialPromise suivra l'index pour lequel une promesse a été rejetée.

la mise en oeuvre

clear();

var http = {
    get(url) {
        var delay = Math.floor( Math.random() * 10 ), even = !(delay % 2);
        var xhr = new Promise(exe);

        console.log(`REQUEST`, url, delay);
        xhr.then( (data) => console.log(`SUCCESS: `, data) ).catch( (data) => console.log(`FAILURE: `, data) );

        function exe(resolve, reject) {
            var action = { 'true': reject, 'false': resolve }[ even ];
            setTimeout( () => action({ url, delay }), (1000 * delay) );
        }

        return xhr;
    }
};

var SequentialPromise = new (function SequentialPromise() {
    var PRIVATE = this;

    return class SequentialPromise {

        constructor(context, action) {
            this.index = 0;
            this.requests = [ ];
            this.context = context;
            this.action = action;

            return this;
        }

        log() {}

        execute(url, ...more) {
            var { context, action, requests } = this;
            var chain = context[action](url);

            requests.push(chain);
            chain.then( (data) => this.index += 1 );

            if (more.length) return chain.then( () => this.execute(...more) );
            return chain;
        }

    };
})();

var sequence = new SequentialPromise(http, 'get');
var urls = [
    'url/name/space/0',
    'url/name/space/1',
    'url/name/space/2',
    'url/name/space/3',
    'url/name/space/4',
    'url/name/space/5',
    'url/name/space/6',
    'url/name/space/7',
    'url/name/space/8',
    'url/name/space/9'
];
var chain = sequence.execute(...urls);
var promises = sequence.requests;

chain.catch( () => console.warn(`EXECUTION STOPPED at ${sequence.index} for ${urls[sequence.index]}`) );

// console.log('>', chain, promises);

Essentiel

SéquentiellePromise

Cody
la source
0

Si à tout moment vous revenez, Promise.reject('something')vous serez jeté dans le bloc catch à la promesse.

promiseOne
  .then((result) => {
    if (!result) {
      return Promise.reject('No result');
    }
    return;
  })
  .catch((err) => {
    console.log(err);
  });

Si la première promesse ne renvoie aucun résultat, vous n'obtiendrez que «Aucun résultat» dans la console.

Dimitar Gospodinov
la source