10 avril 2022

Promesse API

Il y a 6 méthodes statiques dans la classe Promise. Nous allons rapidement couvrir leurs usages ici.

Promise.all

Disons que nous voulons exécuter de nombreuses promesses en parallèle, et attendre qu’elles soient toutes prêtes.

Par exemple, téléchargez plusieurs URLs en parallèle et traitez le contenu lorsque tout est terminé.

C’est à cela que sert Promise.all.

La syntaxe est:

let promise = Promise.all(iterable);

Promise.all prend un itérable (généralement un tableau de promesses) et renvoie une nouvelle promesse.

La nouvelle promesse est résolue lorsque toutes les promesses énumérées sont résolues et que le tableau de leurs résultats devient son résultat.

Par exemple, le Promise.all ci-dessous se règle après 3 secondes, et ensuite son résultat est un tableau [1, 2, 3]:

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // 1,2,3 quand les promesses sont prêtes : chaque promesse apporte un élément du tableau

Veuillez noter que l’ordre des éléments du tableau résultant est le même que celui des promesses sources. Même si la première promesse prend le plus de temps à se résoudre, elle est toujours la première dans le tableau des résultats.

Une astuce courante consiste à mapper un tableau de données de tâches dans un tableau de promesses, puis à l’intégrer dans Promise.all.

Par exemple, si nous avons un tableau d’URLs, nous pouvons tous les récupérer comme ceci:

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

// mappe chaque url à la promesse du fetch
let requests = urls.map(url => fetch(url));

// Promise.all attend jusqu'à ce que toutes les tâches soient résolues
Promise.all(requests)
  .then(responses => responses.forEach(
    response => alert(`${response.url}: ${response.status}`)
  ));

Voici un plus gros exemple avec la récupération des informations des utilisateurs GitHub dans un tableau, par leurs noms (nous pourrions récupérer un tableau d’informations par leurs identifiants, la logique est la même) :

let names = ['iliakan', 'remy', 'jeresig'];

let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));

Promise.all(requests)
  .then(responses => {
    // toutes les réponses sont résolues avec succès
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`); // affiche 200 pour chaque url
    }

    return responses;
  })
  // mappe le tableau de "responses" dans le tableau "response.json()" pour lire leurs contenus
  .then(responses => Promise.all(responses.map(r => r.json())))
  // toutes les réponses JSON sont analysées : "users" est leur tableau
  .then(users => users.forEach(user => alert(user.name)));

Si l’une des promesses est rejetée, la promesse retournée par Promise.all est rejetée immédiatement avec cette erreur.

Par exemple:

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!

Ici, la deuxième promesse est rejetée en deux secondes. Cela conduit au rejet immédiat de Promise.all, donc .catch s’exécute : l’erreur de rejet devient le résultat de l’ensemble Promise.all.

En cas d’erreur, les autres promesses sont ignorées.

Si une promesse est rejetée, Promise.all est immédiatement rejetée, oubliant complètement les autres dans la liste. Leurs résultats sont ignorés.

Par exemple, s’il y a plusieurs appels fetch, comme dans l'exemple ci-dessus, et que l'un d'eux échoue, les autres continueront à s'exécuter, maisPromise.all` ne les considérera plus. Ils vont probablement se résoudre, mais leurs résultats sera ignoré.

Promise.all ne fait rien pour les annuler, car il n’y a pas de concept “d’annulation” dans les promesses. Dansun autre chapitre nous couvrirons AbortController qui peut vous aider avec cela, mais ce n’est pas une partie de l’API Promise.

Promise.all(iterable) autorise toutes les valeurs “régulières” qui ne sont pas une promesse dans iterable

Normallement, Promise.all(...) accepte un itérable (dans la plupart des cas, un tableau) de promesses. Mais si l’un de ces objets n’est pas une promesse, il est transmis au tableau résultant “tel quel”.

Par exemple, ici les résultats sont les suivants [1, 2, 3]:

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2,
  3
]).then(alert); // 1, 2, 3

Ainsi, nous sommes en mesure de passer des valeurs disponibles à Promise.all où cela nous convient.

Promise.allSettled

Un ajout récent
Ceci est un ajout récent au language. Les anciens navigateurs peuvent nécessiter des polyfills.

Promise.all rejette dans son ensemble si une quelconque promesse est rejetée. Cela est bon pour les cas “tout ou rien”, quand on a besoin de tous les résultats pour continuer :

Promise.all([
  fetch('/template.html'),
  fetch('/style.css'),
  fetch('/data.json')
]).then(render); // la méthode "render" a besoin des résultats de tous les "fetchs"

Promise.allSettled attend juste que toutes les promesses se résolvent, quel que soit le résultat. Le tableau résultant a :

  • {status:"fulfilled", value:result} pour les réponses réussies,
  • {status:"rejected", reason:error} pour les erreurs.

Par exemple, nous aimerions récupérer l’information sur les utilisateurs multiples. Même si une demande échoue, les autres nous intéressent.

Utilisons Promise.allSettled:

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });

Les résultats dans la ligne (*) ci-dessus seront:

[
  {status: 'fulfilled', value: ...response...},
  {status: 'fulfilled', value: ...response...},
  {status: 'rejected', reason: ...error object...}
]

Ainsi, pour chaque promesse, nous obtenons son statut et value/error.

Polyfill

Si le navigateur ne prend pas en charge Promise.allSettled, il est facile de le polyfill:

if (!Promise.allSettled) {
  const rejectHandler = reason => ({ status: 'rejected', reason });

  const resolveHandler = value => ({ status: 'fulfilled', value });

  Promise.allSettled = function (promises) {
    const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler));
    return Promise.all(convertedPromises);
  };
}

Dans ce code, promises.map prend les valeurs d’entrée, les transforme en promesses (juste au cas où autre chose qu’une promesse serait transmis) avec p => Promise.resolve(p), puis ajoute le gestionnaire .then à chacun.

Ce gestionnaire transforme un résultat réussi value en {state:'fulfilled', value}, et une erreur reason en {state:'rejected', reason}. C’est exactement le format de Promise.allSettled.

Dorénavant, nous pouvons utiliser Promise.allSettled pour obtenir les résultats ou toutes les promesses données, même si certaines d’entre elles sont rejetées.

Promise.race

Similaire à Promise.all, mais n’attend que la première promesse soit résolue, et obtient son résultat (ou erreur).

La syntaxe est :

let promise = Promise.race(iterable);

Par exemple, ici, le résultat sera 1 :

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

La première promesse a été la plus rapide, donc, elle est devenue le résultat. Après la première promesse faite " vainqueur de la course ", tous les autres résultats/erreurs sont ignorés.

Promise.any

Similar to Promise.race, but waits only for the first fulfilled promise and gets its result. If all of the given promises are rejected, then the returned promise is rejected with AggregateError – a special error object that stores all promise errors in its errors property.

The syntax is:

let promise = Promise.any(iterable);

For instance, here the result will be 1:

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

The first promise here was fastest, but it was rejected, so the second promise became the result. After the first fulfilled promise “wins the race”, all further results are ignored.

Here’s an example when all promises fail:

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000))
]).catch(error => {
  console.log(error.constructor.name); // AggregateError
  console.log(error.errors[0]); // Error: Ouch!
  console.log(error.errors[1]); // Error: Error!
});

As you can see, error objects for failed promises are available in the errors property of the AggregateError object.

Promise.resolve/reject

Les méthodes Promise.resolve et Promise.reject sont rarement nécessaires dans le code moderne, parce que la syntaxe async/await (nous les couvrirons dans un peu plus tard) les rend en quelque sorte obsolètes.

Nous les couvrons ici par souci de clarté, et pour ceux qui ne peuvent pas utiliser async/await pour une quelconque raison.

Promise.resolve

  • Promise.resolve(value) crée une promesse résolue avec le résultat value.

Comme pour:

let promise = new Promise(resolve => resolve(value));

La méthode est utilisée pour la compatibilité, lorsqu’une fonction est censée renvoyer une promesse.

Par exemple, la fonction loadCached ci-dessous récupère l’URL et mémorise (met en cache) son contenu. Pour les appels futurs avec la même URL, elle récupère immédiatement le contenu précédent du cache, mais utilise Promise.resolve pour en faire une promesse, de sorte que la valeur retournée soit toujours une promesse :

let cache = new Map();

function loadCached(url) {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url)); // (*)
  }

  return fetch(url)
    .then(response => response.text())
    .then(text => {
      cache.set(url,text);
      return text;
    });
}

Nous pouvons écrire loadCached(url).then(...), car la fonction est garantie de renvoyer une promesse. Nous pouvons toujours utiliser .then après loadCached. C’est le but de Promise.resolve dans la ligne (*).

Promise.reject

  • Promise.reject(error) crée une promesse rejetée avec error.

Comme pour:

let promise = new Promise((resolve, reject) => reject(error));

En pratique, cette méthode n’est presque jamais utilisée.

Résumé

Il y a 6 méthodes statiques de la classe Promise:

  1. Promise.all(promises) – attend que toutes les promesses se résolvent et retourne un tableau de leurs résultats. Si l’une des promesses données est rejetée, alors elle devient l’erreur de Promise.all, et tous les autres résultats sont ignorés.
  2. Promise.allSettled(promises) (méthode récemment ajoutée) – attend que toutes les promesses se règlent et retourne leurs résultats sous forme de tableau d’objets avec:
    • state: "fulfilled" ou "rejected"
    • value (si rempli) ou reason (en cas de rejet).
  3. Promise.race(promises) – attend que la première promesse soit réglée, et son résultat/erreur devient le résultat.
  4. Promise.any(promises) (méthode récemment ajoutée) – attend que la première promesse se réalise, et son résultat devient le résultat. Si toutes les promesses données sont rejetées, AggregateError devient l’erreur de Promise.any.
  5. Promise.resolve(value) – fait une promesse résolue avec la valeur donnée.
  6. Promise.reject(error) – fait une promesse rejetée avec l’erreur donnée.

De tous ceux-ci, Promise.all est probablement le plus courant dans la pratique.

Carte du tutoriel