Cours JavaScript et TypeScript · Module 4: JavaScript asynchrone

Création et gestion des objets Promise

14 min de lecture
Dans cette leçon9

Dans la leçon précédente, nous avons examiné les fonctions callback et le problème du « callback hell » qui peut survenir avec des opérations asynchrones complexes. Découvrons maintenant une solution plus moderne - les objets Promise, qui offrent une façon plus élégante de gérer l'asynchronicité en JavaScript.

Que sont les objets Promise ?

Une Promise est un objet représentant l'achèvement (ou l'échec) futur d'une opération asynchrone et sa valeur résultante. Tu peux la voir comme une « promesse » de fournir un certain résultat à l'avenir.

Dans le contexte du Parc Jurassique, une Promise est comme l'envoi d'un échantillon d'ADN de dinosaure au laboratoire pour analyse. Le laboratoire ne renvoie pas les résultats immédiatement - il te donne plutôt un numéro de ticket représentant une « promesse » de fournir les résultats une fois l'analyse terminée.

Une Promise peut se trouver dans l'un de ces trois états :

  • Pending - état initial, ni résolue ni rejetée
  • Fulfilled - l'opération s'est terminée avec succès, la Promise a été résolue avec une valeur
  • Rejected - l'opération a échoué, la Promise a été rejetée avec une raison (erreur)

Créer une Promise

Créer une nouvelle Promise en JavaScript ressemble à ceci :

1const dnaAnalysisPromise = new Promise((resolve, reject) => {
2  // Simulation de l'analyse de l'ADN d'un dinosaure (3 secondes)
3  console.log("Analyse de l'échantillon d'ADN de dinosaure...");
4
5  setTimeout(() => {
6    const successRate = Math.random();
7
8    if (successRate > 0.2) {
9      // Analyse réussie
10      const results = {
11        species: "Velociraptor",
12        age: "Environ 68 millions d'années",
13        diet: "Carnivore",
14        geneticMarkers: ["ACGT-237", "TGGC-118", "GACT-339"]
15      };
16
17      resolve(results); // On tient la promesse
18    } else {
19      // Analyse échouée
20      reject(new Error("Échantillon d'ADN endommagé ou insuffisant")); // On rejette la promesse
21    }
22  }, 3000);
23});

Dans l'exemple ci-dessus :

  1. Nous créons un nouvel objet Promise, qui prend une fonction executor avec deux arguments : resolve et reject
  2. À l'intérieur de la fonction executor, nous effectuons l'opération asynchrone (simulée par setTimeout)
  3. Si l'opération réussit, nous appelons resolve(result) pour tenir la promesse
  4. Si l'opération échoue, nous appelons reject(reason) pour rejeter la promesse

Utiliser une Promise - then, catch et finally

Pour gérer le résultat d'une Promise, nous utilisons les méthodes then(), catch() et finally() :

1console.log("Envoi de l'échantillon au laboratoire...");
2
3dnaAnalysisPromise
4  .then(results => {
5    console.log("Analyse de l'ADN réussie !");
6    console.log(`Espèce : ${results.species}`);
7    console.log(`Âge : ${results.age}`);
8    console.log(`Régime : ${results.diet}`);
9    console.log("Marqueurs génétiques :", results.geneticMarkers.join(", "));
10    return results; // On peut retourner une valeur transmise au prochain .then()
11  })
12  .catch(error => {
13    console.error("Erreur d'analyse de l'ADN :", error.message);
14    return "Aucune donnée"; // Retourner une valeur par défaut en cas d'erreur
15  })
16  .finally(() => {
17    console.log("Procédure d'analyse de l'ADN terminée - quel que soit le résultat.");
18    // finally ne prend aucun argument et ne retourne rien
19  });
20
21console.log("Poursuite du travail pendant l'analyse de l'ADN...");

Résultat (en cas de succès) :

1Envoi de l'échantillon au laboratoire...
2Analyse de l'échantillon d'ADN de dinosaure...
3Poursuite du travail pendant l'analyse de l'ADN...
4(après 3 secondes)
5Analyse de l'ADN réussie !
6Espèce : Velociraptor
7Âge : Environ 68 millions d'années
8Régime : Carnivore
9Marqueurs génétiques : ACGT-237, TGGC-118, GACT-339
10Procédure d'analyse de l'ADN terminée - quel que soit le résultat.
  • then() - prend une fonction callback appelée lorsque la Promise est résolue (fulfilled). Le callback reçoit la valeur avec laquelle la Promise a été résolue.
  • catch() - prend une fonction callback appelée lorsque la Promise est rejetée. Le callback reçoit l'erreur avec laquelle la Promise a été rejetée.
  • finally() - prend une fonction callback appelée lorsque la Promise se termine, qu'elle soit résolue ou rejetée. Le callback ne reçoit aucun argument.

Chaînage de Promises

L'un des plus grands avantages des Promises est la possibilité de chaîner des opérations asynchrones. Chaque appel à .then() retourne une nouvelle Promise, ce qui permet de créer des séquences d'opérations asynchrones sans imbrication.

Imaginons le processus de création d'un nouveau dinosaure au Parc Jurassique :

1function fetchDNAFromDatabase(speciesId) {
2  return new Promise((resolve, reject) => {
3    console.log(`Récupération de l'ADN pour l'espèce ID : ${speciesId}...`);
4    setTimeout(() => {
5      if (speciesId > 0) {
6        resolve(`dna_sequence_${speciesId}`);
7      } else {
8        reject(new Error("ID d'espèce invalide"));
9      }
10    }, 1000);
11  });
12}
13
14function prepareEmbryo(dnaSequence) {
15  return new Promise((resolve, reject) => {
16    console.log(`Préparation de l'embryon à partir de la séquence ADN : ${dnaSequence}...`);
17    setTimeout(() => {
18      if (dnaSequence.includes("dna_sequence")) {
19        resolve(`embryo_${dnaSequence}`);
20      } else {
21        reject(new Error("Séquence ADN invalide"));
22      }
23    }, 1500);
24  });
25}
26
27function incubateEgg(embryo) {
28  return new Promise((resolve, reject) => {
29    console.log(`Incubation de l'œuf avec l'embryon : ${embryo}...`);
30    setTimeout(() => {
31      if (embryo.includes("embryo_")) {
32        resolve(`dino_baby_${embryo.replace("embryo_", "")}`);
33      } else {
34        reject(new Error("Embryon invalide"));
35      }
36    }, 2000);
37  });
38}
39
40function raiseDinosaur(baby) {
41  return new Promise((resolve, reject) => {
42    console.log(`Élevage du jeune dinosaure : ${baby}...`);
43    setTimeout(() => {
44      if (baby.includes("dino_baby_")) {
45        resolve(`adult_dino_${baby.replace("dino_baby_", "")}`);
46      } else {
47        reject(new Error("Jeune dinosaure invalide"));
48      }
49    }, 1500);
50  });
51}
52
53// Utilisation d'une chaîne de promises
54console.log("Démarrage du processus de création de dinosaure...");
55
56fetchDNAFromDatabase(5)
57  .then(dna => {
58    console.log(`ADN récupéré : ${dna}`);
59    return prepareEmbryo(dna); // Retourner une nouvelle Promise
60  })
61  .then(embryo => {
62    console.log(`Embryon préparé : ${embryo}`);
63    return incubateEgg(embryo); // Retourner une nouvelle Promise
64  })
65  .then(baby => {
66    console.log(`Dinosaure éclos : ${baby}`);
67    return raiseDinosaur(baby); // Retourner une nouvelle Promise
68  })
69  .then(adultDino => {
70    console.log(`Dinosaure prêt à être introduit dans le parc : ${adultDino}`);
71  })
72  .catch(error => {
73    console.error("Erreur dans le processus de création du dinosaure :", error.message);
74  })
75  .finally(() => {
76    console.log("Processus de création du dinosaure terminé.");
77  });

Compare cela avec la version à base de callbacks, qui ressemblerait à ceci :

1fetchDNAFromDatabase(5, function(error, dna) {
2  if (error) {
3    console.error("Erreur lors de la récupération de l'ADN :", error.message);
4    console.log("Processus de création du dinosaure terminé.");
5    return;
6  }
7
8  console.log(`ADN récupéré : ${dna}`);
9  prepareEmbryo(dna, function(error, embryo) {
10    if (error) {
11      console.error("Erreur lors de la préparation de l'embryon :", error.message);
12      console.log("Processus de création du dinosaure terminé.");
13      return;
14    }
15
16    console.log(`Embryon préparé : ${embryo}`);
17    incubateEgg(embryo, function(error, baby) {
18      if (error) {
19        console.error("Erreur lors de l'incubation de l'œuf :", error.message);
20        console.log("Processus de création du dinosaure terminé.");
21        return;
22      }
23
24      console.log(`Dinosaure éclos : ${baby}`);
25      raiseDinosaur(baby, function(error, adultDino) {
26        if (error) {
27          console.error("Erreur lors de l'élevage du dinosaure :", error.message);
28          console.log("Processus de création du dinosaure terminé.");
29          return;
30        }
31
32        console.log(`Dinosaure prêt à être introduit dans le parc : ${adultDino}`);
33        console.log("Processus de création du dinosaure terminé.");
34      });
35    });
36  });
37});

Comme tu peux le voir, la version avec Promise est bien plus lisible et plus facile à maintenir.

Convertir des fonctions à base de callbacks en Promises

Nous devons souvent travailler avec des API ou des bibliothèques qui utilisent des fonctions à base de callbacks. Nous pouvons les convertir en Promises à l'aide d'une fonction wrapper :

1// Fonction à base de callback (style Node)
2function fetchEnclosureData(enclosureId, callback) {
3  setTimeout(() => {
4    if (enclosureId <= 0) {
5      callback(new Error("ID d'enclos invalide"));
6      return;
7    }
8
9    const enclosureData = {
10      id: enclosureId,
11      name: `Enclos ${enclosureId}`,
12      area: 1000 * enclosureId,
13      type: enclosureId % 2 === 0 ? "Terrestre" : "Aquatique"
14    };
15
16    callback(null, enclosureData);
17  }, 1000);
18}
19
20// Conversion en Promise
21function fetchEnclosureDataPromise(enclosureId) {
22  return new Promise((resolve, reject) => {
23    fetchEnclosureData(enclosureId, (error, data) => {
24      if (error) {
25        reject(error); // Rejeter la Promise en cas d'erreur
26      } else {
27        resolve(data); // Résoudre la Promise avec les données
28      }
29    });
30  });
31}
32
33// Utilisation de la version Promise
34fetchEnclosureDataPromise(3)
35  .then(data => {
36    console.log("Données de l'enclos :", data);
37  })
38  .catch(error => {
39    console.error("Erreur :", error.message);
40  });

Ce pattern est si courant que Node.js propose une fonction intégrée util.promisify() pour convertir automatiquement les fonctions à base de callbacks en fonctions qui retournent des Promises.

Méthodes statiques de Promise

JavaScript fournit plusieurs méthodes statiques sur l'objet Promise, très utiles dans divers scénarios :

Promise.resolve() et Promise.reject()

Ces méthodes créent une Promise immédiatement résolue ou rejetée avec une valeur donnée :

1// Résolution immédiate
2const resolvedPromise = Promise.resolve("Données du dinosaure");
3
4resolvedPromise.then(data => {
5  console.log("Résolue avec :", data); // "Résolue avec : Données du dinosaure"
6});
7
8// Rejet immédiat
9const rejectedPromise = Promise.reject(new Error("Aucun accès au dinosaure"));
10
11rejectedPromise.catch(error => {
12  console.error("Rejetée avec :", error.message); // "Rejetée avec : Aucun accès au dinosaure"
13});

Promise.all()

Prend un tableau de promises et retourne une nouvelle promise qui se résout quand toutes les promises du tableau se résolvent, ou se rejette dès que l'une d'elles se rejette :

1// Récupération des données de trois dinosaures en parallèle
2const dino1Promise = fetchDinosaurData(1);
3const dino2Promise = fetchDinosaurData(2);
4const dino3Promise = fetchDinosaurData(3);
5
6Promise.all([dino1Promise, dino2Promise, dino3Promise])
7  .then(results => {
8    // results est un tableau de résultats dans l'ordre des promises
9    console.log("Toutes les données des dinosaures récupérées :");
10    console.log("Dinosaure 1 :", results[0]);
11    console.log("Dinosaure 2 :", results[1]);
12    console.log("Dinosaure 3 :", results[2]);
13
14    // On peut maintenant traiter toutes les données d'un coup
15    const totalWeight = results.reduce((sum, dino) => sum + dino.weight, 0);
16    console.log(`Poids total de tous les dinosaures : ${totalWeight} kg`);
17  })
18  .catch(error => {
19    // Si une seule promise se rejette, toute l'opération échoue
20    console.error("Échec de la récupération des données de tous les dinosaures :", error.message);
21  });

Promise.race()

Prend un tableau de promises et retourne une nouvelle promise qui se résout ou se rejette avec le résultat de la première promise à se terminer :

1// Simulation de la surveillance de différents systèmes du parc
2function monitorFence() {
3  return new Promise((resolve, reject) => {
4    setTimeout(() => {
5      if (Math.random() > 0.8) {
6        reject(new Error("Panne de clôture dans le secteur B !"));
7      } else {
8        resolve("Clôture en fonctionnement normal");
9      }
10    }, 1000 + Math.random() * 4000); // 1-5 secondes
11  });
12}
13
14function monitorPower() {
15  return new Promise((resolve, reject) => {
16    setTimeout(() => {
17      if (Math.random() > 0.9) {
18        reject(new Error("Panne d'alimentation principale !"));
19      } else {
20        resolve("Alimentation en fonctionnement normal");
21      }
22    }, 1000 + Math.random() * 4000); // 1-5 secondes
23  });
24}
25
26function monitorCameras() {
27  return new Promise((resolve, reject) => {
28    setTimeout(() => {
29      if (Math.random() > 0.7) {
30        reject(new Error("Signal de caméra perdu dans le secteur D !"));
31      } else {
32        resolve("Caméras en fonctionnement normal");
33      }
34    }, 1000 + Math.random() * 4000); // 1-5 secondes
35  });
36}
37
38// On veut connaître la première alerte qui apparaît
39Promise.race([
40  monitorFence(),
41  monitorPower(),
42  monitorCameras()
43])
44  .then(status => {
45    console.log("État du premier système :", status);
46  })
47  .catch(error => {
48    console.error("ALARME ! Premier problème détecté :", error.message);
49    // Déclencher ici les procédures d'urgence
50    triggerEmergencyProcedure();
51  });
52
53function triggerEmergencyProcedure() {
54  console.log("Déclenchement de la procédure d'urgence...");
55  // Code de la procédure d'urgence
56}

Promise.allSettled()

Introduite en ES2020, prend un tableau de promises et retourne une nouvelle promise qui se résout quand toutes les promises du tableau se terminent (résolues ou rejetées) :

1// Surveillance de tous les systèmes du parc
2Promise.allSettled([
3  monitorFence(),
4  monitorPower(),
5  monitorCameras()
6])
7  .then(results => {
8    console.log("État de tous les systèmes :");
9
10    // Chaque résultat a un statut 'fulfilled' ou 'rejected'
11    results.forEach((result, index) => {
12      const systemNames = ["Clôture", "Alimentation", "Caméras"];
13
14      if (result.status === 'fulfilled') {
15        console.log(`${systemNames[index]} : OK - ${result.value}`);
16      } else {
17        console.log(`${systemNames[index]} : ERREUR - ${result.reason.message}`);
18      }
19    });
20
21    // Vérifier si un système a déclenché une alarme
22    const failures = results.filter(result => result.status === 'rejected');
23    if (failures.length > 0) {
24      console.log(`ATTENTION : ${failures.length} pannes système détectées !`);
25      triggerEmergencyProcedure();
26    }
27  });

Gestion des erreurs dans les Promises

Une bonne gestion des erreurs est cruciale, surtout dans le code asynchrone. Les Promises offrent une manière unifiée de gérer les erreurs via la méthode catch() :

1fetchDNAFromDatabase(-1) // ID invalide, provoquera une erreur
2  .then(dna => {
3    console.log(`ADN récupéré : ${dna}`);
4    return prepareEmbryo(dna);
5  })
6  .then(embryo => {
7    console.log(`Embryon préparé : ${embryo}`);
8    return incubateEgg(embryo);
9  })
10  .then(baby => {
11    console.log(`Dinosaure éclos : ${baby}`);
12    return raiseDinosaur(baby);
13  })
14  .then(adultDino => {
15    console.log(`Dinosaure prêt à être introduit dans le parc : ${adultDino}`);
16  })
17  .catch(error => {
18    // Ce catch gère les erreurs de toutes les Promises précédentes
19    console.error("Erreur dans le processus de création du dinosaure :", error.message);
20
21    // On peut ici journaliser les erreurs, avertir l'utilisateur ou prendre des mesures correctives
22    notifyLabForResearch();
23
24    // On peut aussi retourner une valeur transmise au prochain .then()
25    return "Échec de la tentative de création du dinosaure";
26  })
27  .then(message => {
28    // Ce .then() s'exécutera après le .catch()
29    console.log(message); // "Échec de la tentative de création du dinosaure"
30
31    // On peut ici lancer des procédures de nettoyage ou des actions alternatives
32    return restartProcedure();
33  })
34  .catch(error => {
35    // Ce catch gère les erreurs de restartProcedure()
36    console.error("Erreur de redémarrage :", error.message);
37  });
38
39function notifyLabForResearch() {
40  console.log("Notification du laboratoire concernant le problème d'ADN...");
41}
42
43function restartProcedure() {
44  console.log("Redémarrage de la procédure avec un ADN de secours...");
45  return "Procédure redémarrée";
46}

Règles importantes pour la gestion des erreurs dans les Promises :

  1. La méthode catch() intercepte les erreurs de toutes les méthodes then() précédentes
  2. Si une exception est levée à l'intérieur d'une fonction .then(), la Promise sera rejetée avec cette exception
  3. Tu peux avoir plusieurs méthodes catch() dans une chaîne de Promises pour gérer différents types d'erreurs
  4. Après catch(), tu peux poursuivre la chaîne avec d'autres méthodes then()

Pièges courants des Promises

1. Oublier de retourner les Promises

Une erreur fréquente consiste à oublier de retourner une Promise depuis une fonction, ce qui entraîne un comportement inattendu :

1// Faux - on ne retourne pas la Promise depuis la fonction
2function fetchAndSaveData(id) {
3  fetchDinosaurData(id) // Ceci retourne une Promise, mais on ne la propage pas
4    .then(data => {
5      return saveData(data); // Cette Promise n'est pas visible de l'extérieur
6    });
7
8  // La fonction ne retourne explicitement aucune valeur, donc elle retourne undefined par défaut
9}
10
11// Correct - on retourne la Promise
12function fetchAndSaveData(id) {
13  return fetchDinosaurData(id) // On retourne la Promise
14    .then(data => {
15      return saveData(data); // Cette Promise fait maintenant partie de la chaîne
16    });
17}

2. Ne pas gérer les erreurs

Les Promises rejetées non gérées peuvent causer des problèmes difficiles à diagnostiquer :

1// Mauvais - aucune gestion d'erreur
2fetchDinosaurData(999) // L'ID n'existe pas
3  .then(data => {
4    // Ce code ne s'exécutera pas si la Promise est rejetée
5    displayData(data);
6  });
7// Si la Promise est rejetée, l'erreur n'est pas gérée !
8
9// Bon - toujours gérer les erreurs
10fetchDinosaurData(999)
11  .then(data => {
12    displayData(data);
13  })
14  .catch(error => {
15    console.error("Échec de la récupération des données :", error.message);
16    displayError(error);
17  });

3. Promise Hell

Bien que les Promises aident à éviter le Callback Hell, on peut tomber dans un piège similaire en imbriquant des Promises au lieu de les chaîner :

1// Mauvais - imbrication de Promises (Promise Hell)
2fetchDinosaurData(1)
3  .then(dino => {
4    fetchEnclosureData(dino.enclosureId) // Promise imbriquée
5      .then(enclosure => {
6        fetchStaff(enclosure.id) // Encore une Promise imbriquée
7          .then(staff => {
8            console.log("Données du dinosaure :", dino);
9            console.log("Données de l'enclos :", enclosure);
10            console.log("Personnel :", staff);
11          });
12      });
13  });
14
15// Bon - chaînage de Promises
16fetchDinosaurData(1)
17  .then(dino => {
18    dinoData = dino; // Sauvegarder les données pour plus tard
19    return fetchEnclosureData(dino.enclosureId); // Retourner la Promise
20  })
21  .then(enclosure => {
22    enclosureData = enclosure; // Sauvegarder les données pour plus tard
23    return fetchStaff(enclosure.id); // Retourner la Promise
24  })
25  .then(staff => {
26    console.log("Données du dinosaure :", dinoData);
27    console.log("Données de l'enclos :", enclosureData);
28    console.log("Personnel :", staff);
29  })
30  .catch(error => {
31    console.error("Une erreur est survenue :", error.message);
32  });

Résumé

Les Promises sont un mécanisme JavaScript puissant pour gérer les opérations asynchrones, offrant de nombreux avantages par rapport aux callbacks traditionnels :

  1. Meilleure lisibilité - chaînage plutôt qu'imbrication
  2. Meilleure gestion des erreurs - gestion unifiée via .catch()
  3. Flux plus prévisible - enchaînement plus simple des opérations asynchrones
  4. Opérations parallèles facilitées - méthodes comme Promise.all() et Promise.race()

Les Promises constituent la base d'un mécanisme asynchrone encore plus récent en JavaScript - async/await, que nous aborderons dans les prochaines leçons. Au Parc Jurassique, comme en programmation réelle, une bonne gestion des opérations asynchrones est la clé pour construire des systèmes fiables et maintenables.

Dans le prochain exercice, nous verrons comment chaîner les Promises et construire des flux asynchrones plus complexes.

Code de cette leçon : index.js
1// Multi-Species Counter - Species Counter
2console.log("DINOSAUR SPECIES COUNTER\n");
3
4// Function counting dinosaurs by species
5function countSpecies(dinosaurs) {
6  const counter = {};
7
8  for (let dino of dinosaurs) {
9    if (counter[dino]) {
10      counter[dino]++;
11    } else {
12      counter[dino] = 1;
13    }
14  }
15
16  return counter;
17}
18
19// Function displaying report
20function displaySpeciesReport(counter) {
21  console.log("=== SPECIES REPORT ===\n");
22
23  for (let species in counter) {
24    const count = counter[species];
25    const emoji = getSpeciesEmoji(species);
26    console.log(`${emoji} ${species}: ${count} individuals`);
27  }
28
29  const total = Object.values(counter).reduce((sum, count) => sum + count, 0);
30  console.log(`\nTotal: ${total} dinosaurs`);
31}
32
33function getSpeciesEmoji(species) {
34  const emojis = {
35    'T-Rex': '',
36    'Velociraptor': '',
37    'Triceratops': '',
38    'Brachiosaurus': '',
39    'Stegosaurus': '◆'
40  };
41  return emojis[species] || '';
42}
43
44// Test
45const parkDinosaurs = [
46  'T-Rex', 'T-Rex', 'T-Rex',
47  'Velociraptor', 'Velociraptor', 'Velociraptor', 'Velociraptor',
48  'Triceratops', 'Triceratops',
49  'Brachiosaurus',
50  'Stegosaurus', 'Stegosaurus', 'Stegosaurus'
51];
52
53const speciesCount = countSpecies(parkDinosaurs);
54displaySpeciesReport(speciesCount);

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que fait la fonction setTimeout ?

  2. 2. Qu'est-ce que le callback hell ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les étapes de la gestion d'une opération asynchrone en JavaScript :

  • Éditeur de code

    Écris une fonction qui accepte un callback et l'invoque avec des données de dinosaures.

  • Tri horizontal

    Ordonne les éléments d'un appel de fonction différé :

  • Éditeur de code

    Crée une Promise qui se résout après un délai avec les données d'état du dinosaure, ou se rejette si le capteur ne répond pas.

Articles utiles