Promesses JavaScript - rejeter ou lancer

385

J'ai lu plusieurs articles sur ce sujet, mais il n'est toujours pas clair pour moi s'il y a une différence entre Promise.rejectvs et lancer une erreur. Par exemple,

Utilisation de Promise.reject

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            return Promise.reject(new PermissionDenied());
        }
    });

Utilisation de lancer

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            throw new PermissionDenied();
        }
    });

Ma préférence est d'utiliser throwsimplement parce qu'il est plus court, mais je me demandais s'il y avait un avantage l'un par rapport à l'autre.

Naresh
la source
9
Les deux méthodes produisent exactement la même réponse. Le .then()gestionnaire intercepte l'exception levée et la transforme automatiquement en promesse rejetée. Depuis que j'ai lu que les exceptions levées ne sont pas particulièrement rapides à exécuter, je suppose que le retour de la promesse rejetée pourrait être légèrement plus rapide à exécuter, mais vous auriez à concevoir un test dans plusieurs navigateurs modernes si cela était important à savoir. J'utilise personnellement throwcar j'aime la lisibilité.
jfriend00
@webduvet not with Promises - ils sont conçus pour fonctionner avec throw.
joews
15
Un inconvénient throwest qu'il ne résulterait pas en une promesse rejetée s'il était lancé à partir d'un rappel asynchrone, tel qu'un setTimeout. jsfiddle.net/m07van33 @Blondie votre réponse était correcte.
Kevin B
@joews ça ne veut pas dire que c'est bon;)
webduvet
1
Ah, c'est vrai. Une clarification de mon commentaire serait donc "si elle était lancée depuis un rappel asynchrone non promis " . Je savais qu'il y avait une exception à cela, je ne pouvais tout simplement pas me rappeler ce que c'était. Moi aussi, je préfère utiliser throw simplement parce que je le trouve plus lisible et me permet de l'omettre rejectde ma liste de paramètres.
Kevin B

Réponses:

346

Il n'y a aucun avantage à utiliser l'un contre l'autre, mais il y a un cas spécifique où throwcela ne fonctionnera pas. Cependant, ces cas peuvent être résolus.

Chaque fois que vous êtes dans un rappel de promesse, vous pouvez l'utiliser throw. Cependant, si vous êtes dans un autre rappel asynchrone, vous devez utiliser reject.

Par exemple, cela ne déclenchera pas le catch:

new Promise(function() {
  setTimeout(function() {
    throw 'or nah';
    // return Promise.reject('or nah'); also won't work
  }, 1000);
}).catch(function(e) {
  console.log(e); // doesn't happen
});

Au lieu de cela, vous vous retrouvez avec une promesse non résolue et une exception non capturée. C'est un cas où vous voudriez utiliser à la place reject. Cependant, vous pouvez résoudre ce problème de deux manières.

  1. en utilisant la fonction de rejet de la promesse d'origine dans le délai:

new Promise(function(resolve, reject) {
  setTimeout(function() {
    reject('or nah');
  }, 1000);
}).catch(function(e) {
  console.log(e); // works!
});

  1. en promettant le timeout:

function timeout(duration) { // Thanks joews
  return new Promise(function(resolve) {
    setTimeout(resolve, duration);
  });
}

timeout(1000).then(function() {
  throw 'worky!';
  // return Promise.reject('worky'); also works
}).catch(function(e) {
  console.log(e); // 'worky!'
});

Kevin B
la source
54
Il convient de mentionner que les endroits à l'intérieur d'un rappel asynchrone non promis que vous ne pouvez pas utiliser throw error, vous ne pouvez pas non plus utiliser return Promise.reject(err)ce que l'OP nous demandait de comparer. C'est essentiellement pourquoi vous ne devriez pas mettre de rappels asynchrones dans les promesses. Promettez tout ce qui est asynchrone et vous n'aurez plus ces restrictions.
jfriend00 du
9
"Cependant, si vous êtes dans un autre type de rappel" devrait être "Cependant, si vous êtes dans un autre type de rappel asynchrone ". Les rappels peuvent être synchrones (par exemple avec Array#forEach) et avec ceux-ci, lancer à l'intérieur fonctionnerait.
Félix Saparelli
2
@KevinB lisant ces lignes "il y a un cas spécifique où le lancer ne fonctionnera pas." et "Chaque fois que vous êtes dans un rappel de promesse, vous pouvez utiliser throw. Cependant, si vous êtes dans un autre rappel asynchrone, vous devez utiliser le rejet." J'ai le sentiment que les extraits de l'exemple montreront des cas où throwcela ne fonctionnera pas et Promise.rejectest plutôt un meilleur choix. Cependant, les extraits ne sont pas affectés par l'un de ces deux choix et donnent le même résultat indépendamment de ce que vous choisissez. Suis-je en train de manquer quelque chose?
Anshul
2
Oui. si vous utilisez throw dans un setTimeout, le catch ne sera pas appelé. vous devez utiliser celui rejectqui a été transmis au new Promise(fn)rappel.
Kevin B
2
@KevinB merci de rester. L'exemple donné par OP mentionne qu'il voulait spécifiquement comparer return Promise.reject()et throw. Il ne mentionne pas le rejectrappel donné dans la new Promise(function(resolve, reject))construction. Ainsi, alors que vos deux extraits montrent à juste titre quand vous devez utiliser le rappel de résolution, la question de OP n'était pas celle-là.
Anshul
202

Un autre fait important est que reject() NE met PAS fin au flux de contrôle comme le fait une returninstruction. En revanche, throwmet fin au flux de contrôle.

Exemple:

new Promise((resolve, reject) => {
  throw "err";
  console.log("NEVER REACHED");
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));

contre

new Promise((resolve, reject) => {
  reject(); // resolve() behaves similarly
  console.log("ALWAYS REACHED"); // "REJECTED" will print AFTER this
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));

lukyer
la source
51
Eh bien, le point est correct, mais la comparaison est délicate. Parce que normalement, vous devez renvoyer votre promesse rejetée par écrit return reject(), afin que la ligne suivante ne s'exécute pas.
AZ.
7
Pourquoi voudriez-vous le retourner?
lukyer
31
Dans ce cas, return reject()c'est simplement un raccourci pour reject(); returndire ce que vous voulez, c'est mettre fin au flux. La valeur de retour de l' exécuteur (la fonction transmise à new Promise) n'est pas utilisée, c'est donc sans danger.
Félix Saparelli
47

Oui, la plus grande différence est que le rejet est une fonction de rappel qui est exécutée après le rejet de la promesse, alors que throw ne peut pas être utilisé de manière asynchrone. Si vous avez choisi d'utiliser le rejet, votre code continuera à s'exécuter normalement de manière asynchrone tandis que throw donnera la priorité à l'achèvement de la fonction résolveur (cette fonction s'exécutera immédiatement).

Un exemple que j'ai vu qui m'a aidé à clarifier le problème est que vous pouvez définir une fonction de délai d'attente avec rejet, par exemple:

new Promise(_, reject) {
 setTimeout(reject, 3000);
});

Ce qui précède ne serait pas possible d'écrire avec throw.

Dans votre petit exemple, la différence est indiscernable, mais lorsqu'il s'agit d'un concept asynchrone plus compliqué, la différence entre les deux peut être drastique.

Blondie
la source
2
Cela ressemble à un concept clé, mais je ne le comprends pas tel qu'il est écrit. Encore trop nouveau pour Promises, je suppose.
David Spector
43

TLDR: Une fonction est difficile à utiliser lorsqu'elle renvoie parfois une promesse et lève parfois une exception. Lors de l'écriture d'une fonction asynchrone, préférez signaler un échec en renvoyant une promesse rejetée

Votre exemple particulier obscurcit certaines distinctions importantes entre eux:

Étant donné que vous gérez des erreurs dans une chaîne de promesses, les exceptions levées sont automatiquement converties en promesses rejetées. Cela peut expliquer pourquoi ils semblent être interchangeables - ils ne le sont pas.

Considérez la situation ci-dessous:

checkCredentials = () => {
    let idToken = localStorage.getItem('some token');
    if ( idToken ) {
      return fetch(`https://someValidateEndpoint`, {
        headers: {
          Authorization: `Bearer ${idToken}`
        }
      })
    } else {
      throw new Error('No Token Found In Local Storage')
    }
  }

Ce serait un anti-modèle car vous auriez alors besoin de prendre en charge les cas d'erreurs asynchrones et de synchronisation. Cela pourrait ressembler à:

try {
  function onFulfilled() { ... do the rest of your logic }
  function onRejected() { // handle async failure - like network timeout }
  checkCredentials(x).then(onFulfilled, onRejected);
} catch (e) {
  // Error('No Token Found In Local Storage')
  // handle synchronous failure
} 

Pas bon et c'est exactement là où Promise.reject(disponible dans la portée mondiale) vient à la rescousse et se différencie efficacement throw. Le refactoriste devient désormais:

checkCredentials = () => {
  let idToken = localStorage.getItem('some_token');
  if (!idToken) {
    return Promise.reject('No Token Found In Local Storage')
  }
  return fetch(`https://someValidateEndpoint`, {
    headers: {
      Authorization: `Bearer ${idToken}`
    }
  })
}

Cela vous permet désormais d'en utiliser un seul catch()pour les pannes de réseau et le contrôle d'erreur synchrone pour le manque de jetons:

checkCredentials()
      .catch((error) => if ( error == 'No Token' ) {
      // do no token modal
      } else if ( error === 400 ) {
      // do not authorized modal. etc.
      }
maxwell
la source
1
L'exemple d'Op renvoie toujours une promesse, cependant. La question fait référence à savoir si vous devez utiliser Promise.rejectou throwquand vous voulez retourner une promesse rejetée (une promesse qui passera à la suivante .catch()).
Marcos Pereira
@maxwell - Je vous aime par exemple. Dans le même temps, si lors de l'extraction, vous ajouterez un catch et que vous lancerez l'exception, vous pourrez utiliser try ... catch ... Il n'y a pas de monde parfait sur le flux d'exception, mais je pense qu'en utiliser un un motif unique est logique, et la combinaison des motifs n'est pas sûre (alignée avec votre analogie vs analogie anti-motif).
user3053247
1
Excellente réponse mais je trouve ici une faille - ce modèle suppose que toutes les erreurs sont gérées en renvoyant un Promise.reject - que se passe-t-il avec toutes les erreurs inattendues qui pourraient simplement être levées à partir de checkCredentials ()?
chenop
1
Ouais, vous avez raison @chenop - pour attraper ces erreurs inattendues, vous devez les encapsuler dans try / catch still
maxwell
Je ne comprends pas le cas de @ maxwell. Ne pourriez-vous pas simplement le structurer comme vous le faites checkCredentials(x).then(onFulfilled).catch(e) {}et avoir la catchpoignée à la fois le cas de rejet et le cas d'erreur levée?
Ben Wheeler
5

Un exemple à essayer. Remplacez simplement isVersionThrow par false pour utiliser le rejet au lieu du lancer.

const isVersionThrow = true

class TestClass {
  async testFunction () {
    if (isVersionThrow) {
      console.log('Throw version')
      throw new Error('Fail!')
    } else {
      console.log('Reject version')
      return new Promise((resolve, reject) => {
        reject(new Error('Fail!'))
      })
    }
  }
}

const test = async () => {
  const test = new TestClass()
  try {
    var response = await test.testFunction()
    return response 
  } catch (error) {
    console.log('ERROR RETURNED')
    throw error 
  }  
}

test()
.then(result => {
  console.log('result: ' + result)
})
.catch(error => {
  console.log('error: ' + error)
})

Chris Livdahl
la source