Dois-je revenir après une résolution / un rejet anticipé?

262

Supposons que j'ai le code suivant.

function divide(numerator, denominator) {
 return new Promise((resolve, reject) => {

  if(denominator === 0){
   reject("Cannot divide by 0");
   return; //superfluous?
  }

  resolve(numerator / denominator);

 });
}

Si mon objectif est d'utiliser rejectpour sortir tôt, dois-je prendre l'habitude d' returnen prendre aussi immédiatement après?

sam
la source
5
Oui, en raison de l' exécution jusqu'à la fin

Réponses:

371

Le returnbut est de terminer l'exécution de la fonction après le rejet et d'empêcher l'exécution du code après celle-ci.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {

    if (denominator === 0) {
      reject("Cannot divide by 0");
      return; // The function execution ends here 
    }

    resolve(numerator / denominator);
  });
}

Dans ce cas, il empêche l' resolve(numerator / denominator);exécution de ce qui n'est pas strictement nécessaire. Cependant, il est toujours préférable de mettre fin à l'exécution pour éviter un éventuel piège à l'avenir. De plus, c'est une bonne pratique pour éviter d'exécuter du code inutilement.

Contexte

Une promesse peut être dans l'un des 3 états suivants:

  1. en attente - état initial. De l'attente, nous pouvons passer à l'un des autres États
  2. réalisé - opération réussie
  3. rejeté - échec de l'opération

Lorsqu'une promesse est remplie ou rejetée, elle restera indéfiniment dans cet état (réglée). Ainsi, le rejet d'une promesse tenue ou la réalisation d'une promesse rejetée n'auront aucun effet.

Cet exemple d'extrait montre que bien que la promesse ait été tenue après avoir été rejetée, elle est restée rejetée.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    }

    resolve(numerator / denominator);
  });
}

divide(5,0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

Alors, pourquoi devons-nous revenir?

Bien que nous ne puissions pas changer un état de promesse réglé, le rejet ou la résolution n'arrêtera pas l'exécution du reste de la fonction. La fonction peut contenir du code qui créera des résultats déroutants. Par exemple:

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    }
    
    console.log('operation succeeded');

    resolve(numerator / denominator);
  });
}

divide(5, 0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

Même si la fonction ne contient pas un tel code pour le moment, cela crée un éventuel piège futur. Un futur refactor peut ignorer le fait que le code est toujours exécuté après le rejet de la promesse et sera difficile à déboguer.

Arrêt de l'exécution après résolution / rejet:

Il s'agit de flux de contrôle JS standard.

  • Retour après le resolve/ reject:

  • Retour avec le resolve/ reject- puisque la valeur de retour du rappel est ignorée, nous pouvons enregistrer une ligne en renvoyant l'instruction de rejet / résolution:

  • Utilisation d'un bloc if / else:

Je préfère utiliser l'une des returnoptions car le code est plus plat.

Ori Drori
la source
28
Il convient de noter que le code ne se comportera pas différemment s'il returnest présent ou non, car une fois qu'un état de promesse a été défini, il ne peut pas être modifié, donc appeler resolve()après l'appel reject()ne fera rien, sauf utiliser quelques cycles CPU supplémentaires. Moi-même, j'utiliserais le returnjuste du point de vue de la propreté et de l'efficacité du code, mais ce n'est pas obligatoire dans cet exemple spécifique.
jfriend00
1
Essayez d'utiliser à la Promise.try(() => { })place de la nouvelle promesse et évitez d'utiliser les appels de résolution / rejet. Au lieu de cela, vous pouvez simplement écrire return denominator === 0 ? throw 'Cannot divide by zero' : numerator / denominator; que j'utilise Promise.trycomme moyen de lancer une promesse ainsi que d'éliminer les promesses enveloppées dans des blocs try / catch qui sont problématiques.
kingdango
2
C'est bon à savoir et j'aime le motif. Cependant, pour le moment, Promise.try est une suggestion de l'étape 0, vous ne pouvez donc l'utiliser qu'avec une cale ou en utilisant une bibliothèque de promesses comme bluebird ou Q.
Ori Drori
6
@ jfriend00 De toute évidence, dans cet exemple simple, le code ne se comportera pas différemment. Mais que se passe-t-il si vous avez du code après le rejectqui fait quelque chose de cher, comme se connecter à des bases de données ou des points de terminaison API? Tout cela serait inutile et vous coûterait de l'argent et des ressources, en particulier par exemple si vous vous connectez à quelque chose comme une base de données AWS ou un point de terminaison API Gateway. Dans ce cas, vous utiliserez certainement un retour afin d'éviter l'exécution de code inutile.
Jake Wilson
3
@JakeWilson - Bien sûr, c'est juste un flux de code normal en Javascript et n'a rien à voir avec des promesses. Si vous avez terminé de traiter la fonction et que vous ne souhaitez plus exécuter de code dans le chemin de code actuel, vous insérez un return.
jfriend00
37

Un idiome commun, qui peut ou non être votre tasse de thé, consiste à combiner le returnavec le reject, pour rejeter simultanément la promesse et quitter la fonction, de sorte que le reste de la fonction, y compris le, resolvene soit pas exécuté. Si vous aimez ce style, il peut rendre votre code un peu plus compact.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) return reject("Cannot divide by 0");
                           ^^^^^^^^^^^^^^
    resolve(numerator / denominator);
  });
}

Cela fonctionne très bien car le constructeur Promise ne fait rien avec aucune valeur de retour, et en tout cas resolveet rejectne retourne rien.

Le même idiome peut être utilisé avec le style de rappel indiqué dans une autre réponse:

function divide(nom, denom, cb){
  if(denom === 0) return cb(Error("Cannot divide by zero"));
                  ^^^^^^^^^
  cb(null, nom / denom);
} 

Encore une fois, cela fonctionne bien car la personne qui appelle dividene s'attend pas à ce qu'elle retourne quoi que ce soit et ne fait rien avec la valeur de retour.


la source
6
Je n'aime pas ça. Cela donne l'idée que vous retournez quelque chose que vous n'êtes pas en fait. Vous appelez la fonction de rejet et utilisez ensuite la touche retour pour terminer l'exécution de la fonction. Gardez-les sur des lignes séparées, ce que vous faites ne fera que confondre les gens. La lisibilité du code est primordiale.
K - La toxicité du SO augmente.
7
@KarlMorrison, vous retournez en fait "quelque chose", une promesse rejetée. Je pense que la "notion" dont vous parlez est très personnelle. Il n'y a rien de mal à retourner un rejectstatut
Frondor
5
@ Frondor Je ne pense pas que vous ayez compris ce que j'ai écrit. Bien sûr, vous et moi comprenons cela, rien ne se passe lors du retour d'un rejet sur la même ligne. Mais qu'en est-il des développeurs qui ne sont pas tellement habitués à JavaScript entrant dans un projet? Ce type de programmation diminue la lisibilité pour ces personnes. L'écosystème JavaScript est aujourd'hui un gâchis suffisant et les gens qui répandent ce type de pratiques ne feront qu'empirer les choses. C'est une mauvaise pratique.
K - La toxicité du SO augmente.
1
@KarlMorrison Opinions personnelles! = Mauvaise pratique. Cela aiderait probablement un nouveau développeur Javascript à comprendre ce qui se passe avec le retour.
Toby Caulk
1
@TobyCaulk Si les gens ont besoin d'apprendre ce que fait le retour, alors ils ne devraient pas jouer avec Promises, ils devraient apprendre la programmation de base.
K - La toxicité du SO augmente.
10

Techniquement, cela n'est pas nécessaire ici 1 - car une promesse peut être résolue ou rejetée, exclusivement et une seule fois. Le premier résultat Promise l'emporte et chaque résultat suivant est ignoré. Ceci est différent des rappels de style Node.

Cela étant dit, il est de bonne pratique propre de s'assurer que exactement un est appelé, lorsque cela est possible, et en effet dans ce cas, car il n'y a plus de traitement asynchrone / différé. La décision de «revenir tôt» n'est pas différente de la fin d'une fonction lorsque son travail est terminé - par rapport à la poursuite d'un traitement non lié ou inutile.

Le retour au moment opportun (ou l'utilisation de conditions pour éviter d'exécuter «l'autre» cas) réduit les risques d'exécuter accidentellement du code dans un état non valide ou d'effectuer des effets secondaires indésirables; et en tant que tel, il rend le code moins susceptible de «casser de façon inattendue».


1 Cette réponse technique dépend également du fait que dans ce cas le code après le "retour", s'il est omis, n'entraînera pas d'effet secondaire. JavaScript divisera heureusement par zéro et retournera soit + Infinity / -Infinity ou NaN.

user2864740
la source
Belle note de bas de page !!
HankCa
9

Si vous ne "retournez" pas après une résolution / rejet, de mauvaises choses (comme une redirection de page) peuvent se produire après que vous vouliez que cela s'arrête. Source: je suis tombé sur cela.

Benjamin H
la source
6
+1 pour l'exemple. J'ai eu un problème où mon programme ferait plus de 100 requêtes de base de données invalides et je ne pouvais pas comprendre pourquoi. Il s'avère que je ne suis pas "revenu" après un rejet. C'est une petite erreur mais j'ai appris ma leçon.
AdamInTheOculus
8

La réponse d'Ori explique déjà qu'il n'est pas nécessaire de return mais c'est une bonne pratique. Notez que le constructeur de promesses est sûr de lancer, donc il ignorera les exceptions levées passées plus tard dans le chemin, vous avez essentiellement des effets secondaires que vous ne pouvez pas facilement observer.

Notez returnqu'ing early est également très courant dans les rappels:

function divide(nom, denom, cb){
     if(denom === 0){
         cb(Error("Cannot divide by zero");
         return; // unlike with promises, missing the return here is a mistake
     }
     cb(null, nom / denom); // this will divide by zero. Since it's a callback.
} 

Ainsi, bien que ce soit une bonne pratique dans les promesses, elle est requise avec les rappels. Quelques notes sur votre code:

  • Votre cas d'utilisation est hypothétique, n'utilisez pas réellement de promesses avec des actions synchrones.
  • Le constructeur de promesse ignore les valeurs de retour. Certaines bibliothèques vous avertiront si vous renvoyez une valeur non indéfinie pour vous mettre en garde contre l'erreur d'y retourner. La plupart ne sont pas si intelligents.
  • Le constructeur de promesses est sûr de jeter, il convertira les exceptions en rejets, mais comme d'autres l'ont souligné - une promesse se résout une fois.
Benjamin Gruenbaum
la source
4

Dans de nombreux cas, il est possible de valider les paramètres séparément et de renvoyer immédiatement une promesse rejetée avec Promise.reject (raison) .

function divide2(numerator, denominator) {
  if (denominator === 0) {
    return Promise.reject("Cannot divide by 0");
  }
  
  return new Promise((resolve, reject) => {
    resolve(numerator / denominator);
  });
}


divide2(4, 0).then((result) => console.log(result), (error) => console.log(error));

Dorad
la source