web-dev-qa-db-fra.com

Les promesses enchaînées ne transmettent pas le rejet

J'ai du mal à comprendre pourquoi les rejets ne sont pas transmis par le biais d'une chaîne de promesses et j'espère que quelqu'un pourra m'aider à comprendre pourquoi. Pour moi, attacher des fonctionnalités à une chaîne de promesses implique que je compte sur une promesse originale à réaliser. C'est difficile à expliquer, alors laissez-moi d'abord montrer un exemple de code de mon problème. (Remarque: cet exemple utilise Node et le module de nœud différé. J'ai testé cela avec Dojo 1.8.3 et j'ai eu les mêmes résultats)

var d = require("deferred");

var d1 = d();

var promise1 = d1.promise.then(
    function(wins) { console.log('promise1 resolved'); return wins;},
    function(err) { console.log('promise1 rejected'); return err;});
var promise2 = promise1.then(
    function(wins) { console.log('promise2 resolved'); return wins;},
    function(err) { console.log('promise2 rejected'); return err;});
var promise3 = promise2.then(
    function(wins) { console.log('promise3 resolved'); return wins;},
    function(err) { console.log('promise3 rejected'); return err;});
d1.reject(new Error());

Les résultats de l'exécution de cette opération sont les résultats suivants:

promise1 rejected
promise2 resolved
promise3 resolved

D'accord, pour moi, ce résultat n'a pas de sens. En s'attachant à cette chaîne de promesses, chacun implique alors l'intention de dépendre de la résolution réussie de d1 et d'un résultat transmis dans la chaîne. Si la promesse dans promise1 ne reçoit pas la valeur wins, mais obtient à la place une valeur err dans son gestionnaire d'erreurs, comment est-il possible que la prochaine promesse de la chaîne ait sa fonction de succès appelée? Il n'y a aucun moyen qu'il puisse transmettre une valeur significative à la prochaine promesse car il n'a pas obtenu de valeur lui-même.

Une autre façon de décrire ce que je pense est la suivante: il y a trois personnes, John, Ginger et Bob. John possède une boutique de widgets. Ginger entre dans sa boutique et demande un sac de widgets de couleurs assorties. Il ne les a pas en stock, il envoie donc une demande à son distributeur pour les lui faire expédier. Entre-temps, il remet à Ginger un chèque de pluie indiquant qu'il lui doit le sac de widgets. Bob découvre que Ginger obtient les widgets et lui demande d'obtenir le widget bleu quand elle en aura fini avec eux. Elle accepte et lui donne une note indiquant qu'elle le fera. Maintenant, le distributeur de John ne peut pas trouver de widgets dans leur approvisionnement et le fabricant ne les fabrique plus, alors ils informent John, qui à son tour informe Ginger qu'elle ne peut pas obtenir les widgets. Comment Bob peut-il obtenir un widget bleu de Ginger alors qu'il n'en a pas obtenu elle-même?

Voici une troisième perspective plus réaliste que j'ai sur cette question. Disons que j'ai deux valeurs que je veux mettre à jour dans une base de données. L'un dépend de l'ID de l'autre, mais je ne peux pas obtenir l'ID avant de l'avoir déjà inséré dans une base de données et obtenu le résultat. De plus, la première insertion dépend d'une requête de la base de données. Les appels de base de données renvoient des promesses que j'utilise pour enchaîner les deux appels en une séquence.

var promise = db.query({parent_id: value});
promise.then(function(query_result) {
    var first_value = {
        parent_id: query_result[0].parent_id
    }
    var promise = db.put(first_value);
    promise.then(function(first_value_result) {
        var second_value = {
            reference_to_first_value_id: first_value_result.id
        }
        var promise = db.put(second_value);
        promise.then(function(second_value_result) {
            values_successfully_entered();
        }, function(err) { return err });
    }, function(err) { return err });
}, function(err) { return err });

Maintenant, dans cette situation, si la requête db.query échouait, elle appellerait alors la fonction err de la première. Mais alors il appellerait la fonction de réussite de la prochaine promesse. Bien que cette promesse attende les résultats de la première valeur, elle obtiendrait à la place le message d'erreur de sa fonction de gestionnaire d'erreur.

Donc, ma question est, pourquoi aurais-je une fonction de gestion des erreurs si je dois tester les erreurs dans ma fonction de réussite?

Désolé pour la longueur de ceci. Je ne savais tout simplement pas comment l'expliquer autrement.

MISE À JOUR et correction

(Remarque: j'ai supprimé une réponse que j'avais faite à certains commentaires. Donc, si quelqu'un a commenté ma réponse, ses commentaires peuvent sembler hors contexte maintenant que je l'ai supprimée. Désolé pour cela, j'essaie de garder cela aussi court que possible .)

Merci à tous ceux qui ont répondu. Je voudrais d'abord m'excuser auprès de tout le monde d'avoir si mal écrit ma question, en particulier mon pseudo-code. J'étais un peu trop agressif en essayant d'être bref.

Grâce à la réponse de Bergi, je pense avoir trouvé l'erreur dans ma logique. Je pense que j'aurais pu ignorer un autre problème qui causait le problème que j'avais. Cela cause peut-être le fonctionnement de la chaîne de promesses différemment que je ne le pensais. Je teste toujours différents éléments de mon code, donc je ne peux même pas former une bonne question pour voir ce que je fais encore mal. Je voulais tout de même vous mettre à jour et merci pour votre aide.

34
Jordan

Pour moi, ce résultat n'a pas de sens. En s'attachant à cette chaîne de promesses, chacun implique alors l'intention qu'il dépendra de la résolution réussie de d1 et d'un résultat transmis dans la chaîne

Non. Ce que vous décrivez n'est pas une chaîne, mais simplement attachez tous les rappels à d1. Pourtant, si vous voulez chaîner quelque chose avec then, le résultat pour promise2 dépend de la résolution de promise1 et comment les rappels then l'ont géré.

Les documents indiquent:

Renvoie une nouvelle promesse pour le résultat du ou des rappels.

Le .then la méthode est généralement considérée en termes de promesses/spécification A (ou encore plus stricte promesses/A + one ). Cela signifie que les rappels de retour de Shell promettent d'être assimilés pour devenir la résolution de promise2, et s'il n'y a pas de gestionnaire de succès/erreur, le résultat respectif sera en cas passé directement à promise2 - pour que vous puissiez simplement omettre le gestionnaire pour propager l'erreur.

Pourtant, si l'erreur est gérée, le promise2 est considéré comme fixe et sera rempli avec cette valeur. Si vous ne voulez pas cela, vous devrez re -throw l'erreur , tout comme dans une clause try-catch. Vous pouvez également retourner une promesse (à être) rejetée par le gestionnaire. Je ne sais pas quelle est la façon de rejeter Dojo, mais:

var d1 = d();

var promise1 = d1.promise.then(
    function(wins) { console.log('promise1 resolved'); return wins;},
    function(err) { console.log('promise1 rejected'); throw err;});
var promise2 = promise1.then(
    function(wins) { console.log('promise2 resolved'); return wins;},
    function(err) { console.log('promise2 rejected'); throw err;});
var promise3 = promise2.then(
    function(wins) { console.log('promise3 resolved'); return wins;},
    function(err) { console.log('promise3 rejected'); throw err;});
d1.reject(new Error());

Comment Bob peut-il obtenir un widget bleu de Ginger alors qu'il n'en a pas obtenu elle-même?

Il ne devrait pas pouvoir. S'il n'y a pas de gestionnaire d'erreur, il percevra simplement le message (((du distributeur) de John) de Ginger) qu'il n'y a plus de widgets. Pourtant, si Ginger met en place un gestionnaire d'erreurs pour ce cas, elle pourrait toujours remplir sa promesse de donner à Bob un widget en lui donnant un vert de sa propre cabane s'il ne reste plus de bleu chez John ou son distributeur.

Pour traduire vos rappels d'erreur dans le métapher, return err du gestionnaire reviendrait à dire "s'il n'y a plus de widgets, donnez-lui simplement la note qu'il n'en reste plus - c'est aussi bon que le widget souhaité".

Dans la situation de la base de données, si la requête db.query échouait, elle appellerait la fonction err de la première puis

… Ce qui signifierait que l'erreur y est gérée. Si vous ne le faites pas, omettez simplement le rappel d'erreur. Btw, vos rappels de réussite ne font pas return les promesses qu'ils créent, ils semblent donc tout à fait inutiles. Le correct serait:

var promise = db.query({parent_id: value});
promise.then(function(query_result) {
    var first_value = {
        parent_id: query_result[0].parent_id
    }
    var promise = db.put(first_value);
    return promise.then(function(first_value_result) {
        var second_value = {
            reference_to_first_value_id: first_value_result.id
        }
        var promise = db.put(second_value);
        return promise.then(function(second_value_result) {
            return values_successfully_entered();
        });
    });
});

ou, puisque vous n'avez pas besoin des fermetures pour accéder aux valeurs de résultat des rappels précédents, même:

db.query({parent_id: value}).then(function(query_result) {
    return db.put({
        parent_id: query_result[0].parent_id
    });
}).then(function(first_value_result) {
    return db.put({
        reference_to_first_value_id: first_value_result.id
    });
}.then(values_successfully_entered);
26
Bergi

@ Jordan d'abord, comme l'ont noté les commentateurs, lorsque vous utilisez la bibliothèque différée, votre premier exemple produit définitivement le résultat que vous attendez:

promise1 rejected
promise2 rejected
promise3 rejected

Deuxièmement, même si cela produisait une sortie que vous proposez, cela n'affecterait pas le flux d'exécution de votre deuxième extrait, ce qui est un peu différent, plus comme:

promise.then(function(first_value) {
    console.log('promise1 resolved');
    var promise = db.put(first_value);
    promise.then(function (second_value) {
         console.log('promise2 resolved');
         var promise = db.put(second_value);
         promise.then(
             function (wins) { console.log('promise3 resolved'); },
             function (err) { console.log('promise3 rejected'); return err; });
    }, function (err) { console.log('promise2 rejected'); return err;});
}, function (err) { console.log('promise1 rejected'); return err});

et que, en cas de rejet de la première promesse, il suffit de produire:

promise1 rejected

Cependant (accéder à la partie la plus intéressante) même si la bibliothèque différée renvoie définitivement 3 x rejected, La plupart des autres bibliothèques de promesses renverront 1 x rejected, 2 x resolved (Ce qui laisse supposer que vous les avez résultats en utilisant une autre bibliothèque de promesses à la place).

Ce qui est également déroutant, ces autres bibliothèques sont plus correctes avec leur comportement. Laisse-moi expliquer.

Dans un monde de synchronisation, l'équivalent du "rejet de promesse" est throw. Donc, sémantiquement, async deferred.reject(new Error()) en synchronisation est égal à throw new Error(). Dans votre exemple, vous ne lancez pas d'erreurs dans vos rappels de synchronisation, vous les renvoyez simplement, vous passez donc au flux de réussite, une erreur étant une valeur de réussite. Pour vous assurer que le rejet est transmis plus loin, vous devez relancer vos erreurs:

function (err) { console.log('promise1 rejected'); throw err; });

Alors maintenant, la question est, pourquoi la bibliothèque différée a-t-elle pris l'erreur renvoyée comme rejet?

La raison en est que le rejet des travaux différés est un peu différent. Dans la bibliothèque différée, la règle est: la promesse est rejetée lorsqu'elle est résolue avec une instance d'erreur, donc même si vous faites deferred.resolve(new Error()) elle agira comme deferred.reject(new Error()), et si vous essayez de faire deferred.reject(notAnError) il lèvera une exception disant que cette promesse ne peut être rejetée qu'en cas d'erreur. Cela explique pourquoi l'erreur renvoyée par le rappel then rejette la promesse.

Il y a un raisonnement valide derrière la logique différée, mais ce n'est toujours pas comparable à la façon dont throw fonctionne en JavaScript, et à cause de cela, ce comportement est programmé pour changer avec la version v0.7 de différé.

Court résumé:

Pour éviter toute confusion et des résultats inattendus, suivez simplement les règles de bonnes pratiques:

  1. Rejette toujours tes promesses avec des instances d'erreur (suivez les règles du monde de synchronisation, où jeter une valeur qui n'est pas une erreur est considéré comme une mauvaise pratique).
  2. Rejeter des rappels de synchronisation par lancer erreurs (les renvoyer ne garantit pas le rejet).

En obéissant à ce qui précède, vous obtiendrez des résultats cohérents et attendus dans les bibliothèques de promesses différées et les autres bibliothèques de promesses populaires.

1
Mariusz Nowak

L'utilisation peut encapsuler les erreurs à chaque niveau de la promesse. J'ai enchaîné les erreurs dans TraceError:

class TraceError extends Error {
  constructor(message, ...causes) {
    super(message);

    const stack = Object.getOwnPropertyDescriptor(this, 'stack');

    Object.defineProperty(this, 'stack', {
      get: () => {
        const stacktrace = stack.get.call(this);
        let causeStacktrace = '';

        for (const cause of causes) {
          if (cause.sourceStack) { // trigger lookup
            causeStacktrace += `\n${cause.sourceStack}`;
          } else if (cause instanceof Error) {
            causeStacktrace += `\n${cause.stack}`;
          } else {
            try {
              const json = JSON.stringify(cause, null, 2);
              causeStacktrace += `\n${json.split('\n').join('\n    ')}`;
            } catch (e) {
              causeStacktrace += `\n${cause}`;
              // ignore
            }
          }
        }

        causeStacktrace = causeStacktrace.split('\n').join('\n    ');

        return stacktrace + causeStacktrace;
      }
    });

    // access first error
    Object.defineProperty(this, 'cause', {value: () => causes[0], enumerable: false, writable: false});

    // untested; access cause stack with error.causes()
    Object.defineProperty(this, 'causes', {value: () => causes, enumerable: false, writable: false});
  }
}

tilisation

throw new TraceError('Could not set status', srcError, ...otherErrors);

Sortie

Fonctions

TraceError#cause - first error
TraceError#causes - list of chained errors
0
Mathew Kurian