web-dev-qa-db-fra.com

Rompre la chaîne de promesse et appeler une fonction basée sur l’étape de la chaîne où elle est rompue (rejetée)

Mettre à jour:

Pour aider les futurs téléspectateurs de ce post, 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 fonctions stepError(1) AND stepError(2) sont déclenchées. Si je ne sais pas return $q.reject, alors stepError(2) ne sera pas licencié, mais step(2) le sera, ce que je comprends. J'ai tout accompli sauf ce que j'essaie de faire.

Comment puis-je é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 un autre moyen d'accomplir cela?

Voici une démo en direct vous avez donc quelque chose à travailler.

Mettre à jour:

Je en quelque sorte l'ai résolu. Ici, je détecte l'erreur à la fin de la chaîne et passe les données à reject(data) afin que je sache quel problème gérer dans la fonction d'erreur. En réalité, cela ne répond pas à mes exigences car je ne veux pas dépendre des données. Ce serait boiteux, mais dans mon cas, il serait plus simple de transmettre un rappel d'erreur à la fonction plutôt que de dépendre des données renvoyées pour déterminer quoi faire.

Live demo here (click).

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); 
  }
110
m59

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

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 des blocs trycatch:

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 gestionnaire onRejected (le deuxième argument de then) est essentiellement un mécanisme de correction d'erreur (comme un bloc catch). Si une erreur est générée dans handleErrorOne, elle sera interceptée par le prochain bloc d'interception (catch(e2)), etc.

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 se passe mal:

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

Remarque: Nous pouvons laisser la variable handleErrorOne telle quelle, car elle ne sera invoquée que si la variable stepOne est rejetée (c'est la première fonction de la chaîne. Nous savons donc que si la chaîne est rejetée à ce stade-ci, cela ne peut être que dû promettre).

Le changement important est que les gestionnaires d’erreur 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 onRejected qui n'est appelée que si l'étape a été rejetée (mais ne peut pas être atteinte directement par la chaîne principale).

Cela fonctionne parce que onFulfilled et onRejected sont des arguments facultatifs de la méthode then. Si une promesse est remplie (c'est-à-dire résolue) et que la prochaine variable then de la chaîne ne possède pas de gestionnaire onFulfilled, la chaîne se poursuivra jusqu'à ce qu'il y en ait une 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 est pas équivalente aux deux précédentes:

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

La bibliothèque de promesses d'Angular $q est basée sur la bibliothèque Q de kriskowal (qui possède une API plus riche, mais contient tout ce que vous pouvez trouver dans $q). Q's Documentation de l'API sur GitHub pourrait s'avérer utile. Q implémente le Promises/A + spec , qui explique en détail comment then et le comportement de résolution de promesse fonctionnent exactement.

MODIFIER:

N'oubliez pas non plus que si vous voulez sortir de la chaîne dans votre gestionnaire d'erreurs, il doit renvoyer une promesse refusée ou renvoyer une erreur (qui sera automatiquement interceptée dans une promesse refusée). Si vous ne renvoyez pas de promesse, then encapsule la valeur de retour dans une promesse de résolution. 

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

165
Alan Plum

Un peu tard pour 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 rompre sortir de la chaîne.

44
Vinnyq12

Ce dont vous avez besoin est une chaîne répétitive .then() avec un cas spécial pour commencer et un cas spécial pour terminer.

Le talent consiste à faire en sorte que le numéro d'étape du cas d'échec se répercute sur un gestionnaire d'erreurs final.

  • Démarrer: appelez step(1) inconditionnellement.
  • Motif répété: chaînez une .then() avec les rappels suivants:
    • succès: appelez step (n + 1)
    • Échec: renvoie la valeur avec laquelle le différé précédent a été rejeté ou retransmet l'erreur.
  • Terminer: chaine une .then() sans gestionnaire de succès et un gestionnaire d'erreur final.

Vous pouvez écrire le tout dans son intégralité, mais il est plus facile de démontrer le motif à l'aide de fonctions généralisées nommé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 démo

Notez comment dans step(), le différé est rejeté ou résolu avec n, rendant ainsi cette valeur disponible pour les rappels dans la prochaine .then() de la chaîne. Une fois que stepError est appelé, l'erreur est répétée jusqu'à ce qu'elle soit traitée par finalError.

9
Beetroot-Beetroot

Lorsque vous refusez, vous devez transmettre une erreur de rejet, puis envelopper les gestionnaires d'erreur d'étape dans une fonction qui vérifie si le rejet doit être traité ou "repris" 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 du code de travail https://jsfiddle.net/8hzg5s7m/3/

Si vous avez un traitement 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);    
    }
}

alors votre 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);
});
6
redben

Si je comprends bien, vous ne voulez que l'erreur à afficher pour l'étape en échec, n'est-ce pas?

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 renvoyant $q.reject() dans le cas d'échec de la première étape, vous refusez cette promesse, ce qui entraîne l'appel de errorCallback dans la 2nd then(...).

2
Zajn
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 un nombre quelconque 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

2
Esailija

Associez des gestionnaires d’erreur en tant qu’éléments de chaîne distincts 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(); });
});

Note: Ceci est fondamentalement le même modèle que suggère Pluma dans sa réponse mais en utilisant le nom du PO.

1
Ignitor

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

(La réponse acceptée mentionne then(null, onErrorHandler), qui est fondamentalement identique à catch(onErrorHandler).)

Utiliser et enchaîner 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');
});

Des pièges quand on lance des 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
});

Si c'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);
});
1
toraritte

Si vous voulez résoudre ce problème en utilisant async/wait:

(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 []
    }

})()
1
luispa

La meilleure solution consiste à refactoriser votre chaîne de promesses pour utiliser l'attente ES6. Ensuite, vous pouvez simplement revenir de la fonction pour ignorer le reste du comportement.

Cela fait plus d'un an que je me heurte à la tête contre ce modèle et j'attends que l'attente soit le paradis.

0
Pete Alvin

Essayez de l'utiliser comme ceci:

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);
});
0
Leonid