Cours JavaScript et TypeScript · Module 4: JavaScript asynchrone

Async/Await - du code asynchrone élégant

19 min de lecture
Dans cette leçon14

Dans les exercices précédents, nous avons découvert les callbacks et les Promises. Il est temps maintenant d'apprendre async/await - une syntaxe introduite en ES2017 qui fait que le code asynchrone ressemble et se comporte presque comme du code synchrone.

Au Parc Jurassique, imagine que tu es un scientifique qui doit : collecter un échantillon d'ADN, le séquencer, synthétiser une protéine, faire croître des cellules et enfin créer un dinosaure. Avec async/await, ce processus se lit comme une simple procédure étape par étape, même si chaque étape est asynchrone !

Le mot-clé async

Placer async devant une déclaration de fonction en fait une fonction async. Une fonction async :

  • Retourne toujours une Promise (même si tu fais return d'une valeur simple)
  • Peut utiliser le mot-clé await dans son corps
1// Fonction ordinaire - retourne undefined
2function getName() {
3  return "Rexy";
4}
5
6// Fonction async - retourne Promise<string>
7async function getDinosaurName() {
8  return "Rexy";  // Automatiquement enveloppé dans Promise.resolve("Rexy")
9}
10
11getName();                  // "Rexy"
12getDinosaurName();          // Promise { 'Rexy' }
13getDinosaurName().then(name => console.log(name)); // "Rexy"

Le mot-clé await

await ne peut être utilisé qu'à l'intérieur d'une fonction async. Il met en pause l'exécution de la fonction async jusqu'à ce que la Promise se termine, puis reprend avec la valeur résolue.

1function fetchDinosaurData(id) {
2  return new Promise(resolve => {
3    setTimeout(() => resolve({ id, name: "Blue", species: "Velociraptor", health: 98 }), 1000);
4  });
5}
6
7// Sans async/await - avec .then()
8function showDinoInfoOld(id) {
9  return fetchDinosaurData(id).then(dino => {
10    console.log(`${dino.name} (${dino.species}) - santé : ${dino.health}%`);
11    return dino;
12  });
13}
14
15// Avec async/await - se lit comme du code synchrone
16async function showDinoInfo(id) {
17  const dino = await fetchDinosaurData(id); // Met en pause ici jusqu'à la résolution de la Promise
18  console.log(`${dino.name} (${dino.species}) - santé : ${dino.health}%`);
19  return dino;
20}
21
22showDinoInfo(1);

Comparaison : chaînes de Promises vs async/await

Comparons la même fonctionnalité implémentée avec la syntaxe Promise traditionnelle et avec async/await :

1// Avec les Promises traditionnelles
2function checkSecuritySystems() {
3  console.log("Vérification des systèmes de sécurité...");
4
5  return fetchGateStatus()
6    .then(gateStatus => {
7      console.log("État des portes :", gateStatus.allClosed ? "Toutes fermées" : "ATTENTION ! Certaines sont ouvertes");
8
9      return fetchFenceStatus();
10    })
11    .then(fenceStatus => {
12      console.log("Clôture :", fenceStatus.power > 8000 ? "Sécurisée" : "Alimentation insuffisante !");
13
14      return fetchCameraStatus();
15    })
16    .then(cameraStatus => {
17      console.log("Caméras :", cameraStatus.allOperational ? "Toutes opérationnelles" : "Certaines ne fonctionnent pas !");
18
19      return {
20        gate: gateStatus,
21        fence: fenceStatus,
22        cameras: cameraStatus,
23        checkDuration: new Date().toISOString()
24      };
25    })
26    .catch(error => {
27      console.error("Erreur pendant la vérification :", error.message);
28      throw new Error(`Échec de la vérification : ${error.message}`);
29    });
30}
31
32// Avec async/await
33async function checkSecuritySystems() {
34  console.log("Vérification des systèmes de sécurité...");
35
36  try {
37    // Chacun des appels suivants met en pause l'exécution de la fonction
38    const gateStatus = await fetchGateStatus();
39    console.log("État des portes :", gateStatus.allClosed ? "Toutes fermées" : "ATTENTION ! Certaines sont ouvertes");
40
41    const fenceStatus = await fetchFenceStatus();
42    console.log("Clôture :", fenceStatus.power > 8000 ? "Sécurisée" : "Alimentation insuffisante !");
43
44    const cameraStatus = await fetchCameraStatus();
45    console.log("Caméras :", cameraStatus.allOperational ? "Toutes opérationnelles" : "Certaines ne fonctionnent pas !");
46
47    return {
48      gate: gateStatus,
49      fence: fenceStatus,
50      cameras: cameraStatus,
51      checkDuration: new Date().toISOString()
52    };
53  } catch (error) {
54    console.error("Erreur pendant la vérification :", error.message);
55    throw new Error(`Échec de la vérification : ${error.message}`);
56  }
57}

Remarque que :

  • Le code utilisant async/await est nettement plus lisible - il ressemble presque à du code synchrone
  • Il est plus facile de suivre le flux des données (les valeurs gateStatus, fenceStatus, cameraStatus)
  • La gestion des erreurs est plus simple, avec la syntaxe standard try/catch au lieu de .catch()

Convertir des fonctions retournant une Promise en async/await

Toute fonction qui retourne une Promise peut être transformée en fonction async :

1// Fonction retournant une Promise
2function measureEnclosureTemperature(enclosureId) {
3  return new Promise((resolve, reject) => {
4    // Code de mesure...
5  });
6}
7
8// Transformée en fonction async
9async function measureEnclosureTemperature(enclosureId) {
10  // On peut utiliser des opérations asynchrones avec await
11  const sensor = await connectToSensor(enclosureId);
12  const reading = await sensor.getTemperature();
13
14  if (reading.status === "error") {
15    throw new Error(`Erreur de lecture de température : ${reading.message}`);
16  }
17
18  return {
19    enclosureId,
20    temperature: reading.value,
21    unit: "Celsius",
22    measurementTime: new Date()
23  };
24}

Gestion des erreurs avec try/catch

Au lieu de .catch(), les fonctions async utilisent un try/catch classique :

1async function performHealthCheck(dinoId) {
2  try {
3    const dino = await fetchDinosaurData(dinoId);
4    const enclosure = await fetchEnclosureStatus(dino.enclosureId);
5    const vetReport = await fetchVetReport(dinoId);
6
7    return {
8      dinosaur: dino.name,
9      enclosureOk: enclosure.status === "secure",
10      lastCheckup: vetReport.date,
11      overall: "healthy"
12    };
13  } catch (error) {
14    console.error(`Échec du bilan de santé pour le dinosaure ${dinoId} : ${error.message}`);
15    return { dinosaur: dinoId, overall: "unknown", error: error.message };
16  } finally {
17    console.log(`Routine de bilan de santé terminée pour le dinosaure ${dinoId}`);
18  }
19}
20
21// Fonctions auxiliaires
22function fetchEnclosureStatus(id) {
23  return Promise.resolve({ id, status: "secure", lastInspected: "2024-01-15" });
24}
25function fetchVetReport(id) {
26  return Promise.resolve({ dinoId: id, date: "2024-01-10", notes: "All clear" });
27}
28
29performHealthCheck(42).then(report => console.log(report));

Exemple détaillé de gestion des erreurs

1async function releaseDinosaurFromEnclosure(dinoId, enclosureId) {
2  try {
3    // Vérifier si nous avons les autorisations
4    const permissions = await checkPermissions();
5    if (!permissions.canReleaseAnimals) {
6      throw new Error("Pas d'autorisation pour relâcher des dinosaures");
7    }
8
9    // Vérifier l'état du dinosaure
10    const dinosaurStatus = await checkDinosaurStatus(dinoId);
11    if (dinosaurStatus.health !== "Good") {
12      throw new Error(`Le dinosaure n°${dinoId} n'est pas en bon état de santé`);
13    }
14
15    // Vérifier si l'enclos est sécurisé
16    const enclosureStatus = await checkEnclosureStatus(enclosureId);
17    if (!enclosureStatus.isSecure) {
18      throw new Error(`L'enclos n°${enclosureId} n'est pas sécurisé`);
19    }
20
21    // Ouvrir la porte de l'enclos
22    await openEnclosureGateById(enclosureId);
23    console.log(`Porte de l'enclos n°${enclosureId} ouverte`);
24
25    // Relâcher le dinosaure
26    const result = await transferDinosaur(dinoId, enclosureId, "outdoor-area");
27    console.log(`Le dinosaure n°${dinoId} a été relâché dans la zone extérieure de l'enclos n°${enclosureId}`);
28
29    return {
30      success: true,
31      message: `Le dinosaure n°${dinoId} a été relâché avec succès`,
32      details: result
33    };
34  } catch (error) {
35    console.error("L'opération de relâchement du dinosaure a échoué :", error.message);
36
37    // On peut tenter de corriger l'erreur ou effectuer des opérations de nettoyage
38    try {
39      await closeAllGates();
40      await activateSecuritySystem();
41    } catch (cleanupError) {
42      console.error("Erreur pendant les opérations de nettoyage :", cleanupError.message);
43    }
44
45    return {
46      success: false,
47      message: `Erreur lors du relâchement du dinosaure : ${error.message}`,
48      error: error.message
49    };
50  }
51}

Dans l'exemple ci-dessus, nous avons utilisé un bloc try/catch pour gérer toutes les erreurs susceptibles de survenir pendant le processus de relâchement du dinosaure. Nous pouvons aussi imbriquer des blocs try/catch, comme dans le gestionnaire d'erreurs où nous tentons des opérations de nettoyage qui peuvent elles aussi échouer.

Itérations asynchrones

L'introduction d'async/await a aussi permis les itérations asynchrones, particulièrement utiles lorsqu'on travaille avec des collections d'objets nécessitant des opérations asynchrones.

Traitement séquentiel d'une collection

1async function examineAllDinosaurs(idList) {
2  console.log(`Démarrage de l'examen de ${idList.length} dinosaures...`);
3
4  const results = [];
5
6  // Traitement séquentiel (l'un après l'autre)
7  for (const id of idList) {
8    try {
9      const result = await examineDinosaur(id);
10      console.log(`Dinosaure n°${id} examiné : ${result.status}`);
11      results.push({
12        id,
13        success: true,
14        result: result
15      });
16    } catch (error) {
17      console.error(`Erreur lors de l'examen du dinosaure n°${id} : ${error.message}`);
18      results.push({
19        id,
20        success: false,
21        error: error.message
22      });
23    }
24  }
25
26  return {
27    totalExaminations: idList.length,
28    successful: results.filter(w => w.success).length,
29    unsuccessful: results.filter(w => !w.success).length,
30    results
31  };
32}

Dans l'exemple ci-dessus, nous utilisons for...of avec await à l'intérieur de la boucle, ce qui fait que les examens des dinosaures sont effectués l'un après l'autre. C'est l'approche appropriée quand l'ordre compte ou que les opérations doivent être effectuées séquentiellement.

Traitement parallèle d'une collection

Si les opérations n'ont pas besoin d'être effectuées séquentiellement, il est souvent préférable de traiter la collection en parallèle :

1async function examineAllDinosaursInParallel(idList) {
2  console.log(`Démarrage de l'examen de ${idList.length} dinosaures en parallèle...`);
3
4  // Création d'un tableau de promises
5  const promises = idList.map(id => {
6    return examineDinosaur(id)
7      .then(result => ({
8        id,
9        success: true,
10        result: result
11      }))
12      .catch(error => ({
13        id,
14        success: false,
15        error: error.message
16      }));
17  });
18
19  // Exécution parallèle de tous les examens
20  const results = await Promise.all(promises);
21
22  return {
23    totalExaminations: idList.length,
24    successful: results.filter(w => w.success).length,
25    unsuccessful: results.filter(w => !w.success).length,
26    results
27  };
28}

Dans ce cas, nous utilisons map() pour créer un tableau de Promises, puis Promise.all() avec await pour attendre la fin de toutes les opérations. Ainsi, tous les examens sont effectués en parallèle, ce qui peut accélérer considérablement le traitement de grandes collections.

Traitement parallèle avec une limite

Parfois, nous ne voulons pas effectuer toutes les opérations simultanément car cela pourrait surcharger le système. Dans ces cas, nous pouvons limiter le nombre d'opérations parallèles :

1async function examineDinosaursWithLimit(idList, maxParallel = 3) {
2  console.log(`Démarrage de l'examen de ${idList.length} dinosaures (max ${maxParallel} en parallèle)...`);
3
4  const results = [];
5
6  // Traitement par lots
7  for (let i = 0; i < idList.length; i += maxParallel) {
8    const batch = idList.slice(i, i + maxParallel);
9    console.log(`Traitement du lot ${Math.floor(i / maxParallel) + 1} (${batch.length} dinosaures)...`);
10
11    // Création du tableau de promises pour le lot courant
12    const batchPromises = batch.map(id => {
13      return examineDinosaur(id)
14        .then(result => ({
15          id,
16          success: true,
17          result: result
18        }))
19        .catch(error => ({
20          id,
21          success: false,
22          error: error.message
23        }));
24    });
25
26    // Exécution parallèle des examens du lot courant
27    const batchResults = await Promise.all(batchPromises);
28    results.push(...batchResults);
29
30    console.log(`Lot ${Math.floor(i / maxParallel) + 1} terminé.`);
31  }
32
33  return {
34    totalExaminations: idList.length,
35    successful: results.filter(w => w.success).length,
36    unsuccessful: results.filter(w => !w.success).length,
37    results
38  };
39}

Dans ce cas, nous traitons la liste d'IDs par lots de maxParallel éléments. Chaque lot est traité en parallèle, mais nous attendons la fin de toutes les opérations d'un lot avant de passer au suivant.

Exécution séquentielle vs parallèle

L'un des concepts les plus importants avec async/await est de savoir quand l'exécution est séquentielle et quand elle est parallèle :

1// SÉQUENTIEL - chaque await attend avant que le suivant ne démarre
2async function sequentialCheck() {
3  console.time("sequential");
4  const fence = await checkFenceSystem("A");   // ~1 seconde
5  const camera = await checkCameraSystem("Nord"); // ~1 seconde
6  const sensor = await checkMotionSensors("B");    // ~1 seconde
7  console.timeEnd("sequential"); // ~3 secondes au total
8  return [fence, camera, sensor];
9}
10
11// PARALLÈLE - toutes démarrent en même temps, on attend tous les résultats
12async function parallelCheck() {
13  console.time("parallel");
14  const [fence, camera, sensor] = await Promise.all([
15    checkFenceSystem("A"),
16    checkCameraSystem("Nord"),
17    checkMotionSensors("B")
18  ]);
19  console.timeEnd("parallel"); // ~1 seconde au total
20  return [fence, camera, sensor];
21}
22
23// Fonctions auxiliaires
24function checkFenceSystem(id) { return new Promise(res => setTimeout(() => res({ sector: id, ok: true }), 800)); }
25function checkCameraSystem(id) { return new Promise(res => setTimeout(() => res({ zone: id, ok: true }), 900)); }
26function checkMotionSensors(id) { return new Promise(res => setTimeout(() => res({ area: id, ok: true }), 700)); }

Règle de pouce :

  • N'utilise await un par un que lorsque chaque opération dépend du résultat de la précédente
  • Utilise Promise.all() avec await quand les opérations sont indépendantes - c'est bien plus rapide
1async function prepareDinoReport(dinoId) {
2  // Étape 1 : doit se produire en premier (ID nécessaire à l'étape 2)
3  const dino = await fetchDinosaurData(dinoId);
4
5  // Étapes 2 à 4 indépendantes les unes des autres - exécutées en parallèle
6  const [enclosure, vetRecord, feedingLog] = await Promise.all([
7    fetchEnclosureStatus(dino.enclosureId),
8    fetchVetReport(dino.id),
9    fetchFeedingLog(dino.id)
10  ]);
11
12  return { dino, enclosure, vetRecord, feedingLog };
13}
14
15function fetchFeedingLog(id) {
16  return Promise.resolve({ dinoId: id, lastFed: "2024-01-15 08:00", amount: "50kg" });
17}

Fonctions fléchées async et méthodes de classe

async fonctionne avec toutes les formes de fonctions :

1// Fonction fléchée async
2const getDinoStatus = async (id) => {
3  const data = await fetchDinosaurData(id);
4  return data.health > 80 ? "healthy" : "needs attention";
5};
6
7// Méthode de classe async
8class DinoMonitor {
9  constructor(parkId) {
10    this.parkId = parkId;
11  }
12
13  async runFullScan() {
14    console.log(`Démarrage du scan complet du parc ${this.parkId}...`);
15    const results = await Promise.all([
16      this.checkAllFences(),
17      this.checkAllCameras()
18    ]);
19    return results;
20  }
21
22  async checkAllFences() {
23    // Simuler la vérification des clôtures
24    await new Promise(res => setTimeout(res, 600));
25    return { fences: "all secure", checked: 24 };
26  }
27
28  async checkAllCameras() {
29    await new Promise(res => setTimeout(res, 400));
30    return { cameras: "all online", checked: 48 };
31  }
32}
33
34const monitor = new DinoMonitor("JP-001");
35monitor.runFullScan().then(results => console.log("Résultats du scan :", results));

Exemple détaillé de classe : système de surveillance des dinosaures

1class DinosaurMonitoringSystem {
2  constructor(sector) {
3    this.sector = sector;
4    this.active = false;
5    this.dinosaurs = [];
6  }
7
8  async initialize() {
9    try {
10      // Récupérer la liste des dinosaures du secteur
11      const list = await fetchDinosaurListInSector(this.sector);
12      this.dinosaurs = list;
13
14      // Initialiser les capteurs
15      await this.initializeSensors();
16
17      this.active = true;
18      console.log(`Système de surveillance du secteur ${this.sector} initialisé`);
19
20      return true;
21    } catch (error) {
22      console.error(`Erreur d'initialisation du système de surveillance : ${error.message}`);
23      return false;
24    }
25  }
26
27  async initializeSensors() {
28    // Initialiser les capteurs pour chaque dinosaure
29    const promises = this.dinosaurs.map(dino =>
30      initializeSensorForDinosaur(dino.id)
31    );
32
33    return Promise.all(promises);
34  }
35
36  async monitor(monitoringDuration = 60000) {
37    if (!this.active) {
38      throw new Error("Le système de surveillance n'est pas actif");
39    }
40
41    console.log(`Démarrage de la surveillance du secteur ${this.sector} pendant ${monitoringDuration/1000} secondes...`);
42
43    // Surveiller pendant une durée déterminée
44    const startTime = Date.now();
45
46    while (Date.now() - startTime < monitoringDuration) {
47      // Récupérer l'état de tous les dinosaures
48      const statuses = await Promise.all(
49        this.dinosaurs.map(dino => checkDinosaurStatus(dino.id))
50      );
51
52      // Analyser les états
53      const abnormalDinosaurs = statuses.filter(s => s.status !== "normal");
54
55      if (abnormalDinosaurs.length > 0) {
56        console.warn(`${abnormalDinosaurs.length} dinosaures détectés avec un état anormal !`);
57
58        for (const status of abnormalDinosaurs) {
59          console.warn(`Dinosaure n°${status.dinoId} : ${status.status} - ${status.details}`);
60
61          // Envoyer une alerte pour chaque état anormal
62          await sendAlert(this.sector, status);
63        }
64      }
65
66      // Attendre 5 secondes avant la prochaine vérification
67      await new Promise(resolve => setTimeout(resolve, 5000));
68    }
69
70    console.log(`Surveillance du secteur ${this.sector} terminée.`);
71    return `Surveillance terminée : ${new Date().toISOString()}`;
72  }
73}
74
75// Utilisation de la classe
76async function startMonitoringSystem(sector) {
77  const system = new DinosaurMonitoringSystem(sector);
78
79  const initializationSuccessful = await system.initialize();
80  if (!initializationSuccessful) {
81    console.error(`Échec de l'initialisation du système pour le secteur ${sector}`);
82    return;
83  }
84
85  // Surveiller pendant 5 minutes
86  await system.monitor(300000);
87
88  console.log(`Session de surveillance du secteur ${sector} terminée`);
89}

IIFE async (expression de fonction immédiatement invoquée)

Parfois, tu dois exécuter du code async au niveau supérieur (en dehors de toute fonction async). Utilise une IIFE async :

1// Exécute immédiatement la fonction async
2(async () => {
3  try {
4    const status = await getDinoStatus(7);
5    console.log("Statut :", status);
6
7    const report = await prepareDinoReport(7);
8    console.log("Rapport prêt pour le dinosaure :", report.dino.name);
9  } catch (error) {
10    console.error("Erreur de niveau supérieur :", error.message);
11  }
12})();

Fonctions async récursives

Les fonctions async peuvent s'appeler elles-mêmes de façon récursive :

1async function retryOperation(operation, maxRetries, delay = 500) {
2  for (let attempt = 1; attempt <= maxRetries; attempt++) {
3    try {
4      const result = await operation();
5      console.log(`Succès à la tentative ${attempt}`);
6      return result;
7    } catch (error) {
8      console.log(`Tentative ${attempt} échouée : ${error.message}`);
9      if (attempt === maxRetries) throw error;
10      await new Promise(res => setTimeout(res, delay * attempt)); // Backoff quasi exponentiel
11    }
12  }
13}
14
15// Utilisation - réessayer la lecture du capteur jusqu'à 3 fois
16async function readUnreliableSensor(sensorId) {
17  return retryOperation(
18    () => new Promise((resolve, reject) => {
19      setTimeout(() => {
20        Math.random() > 0.6
21          ? resolve({ sensorId, value: Math.random() * 100 })
22          : reject(new Error(`Le capteur ${sensorId} n'a pas répondu`));
23      }, 300);
24    }),
25    3 // Max 3 tentatives
26  );
27}
28
29readUnreliableSensor("THERMO-07")
30  .then(data => console.log("Lecture finale :", data.value.toFixed(1)))
31  .catch(error => console.error("Toutes les tentatives ont échoué :", error.message));

Générateurs async

Les générateurs async combinent les générateurs avec des opérations asynchrones - parfaits pour le streaming de données :

1async function* streamDinoUpdates(dinoIds, intervalMs) {
2  for (const id of dinoIds) {
3    // Simuler la récupération d'une mise à jour en direct pour chaque dinosaure
4    await new Promise(res => setTimeout(res, intervalMs));
5    const update = await fetchDinosaurData(id);
6    yield { timestamp: new Date().toISOString(), dinosaur: update };
7  }
8}
9
10// Consommer le générateur async
11async function monitorLive() {
12  const generator = streamDinoUpdates([1, 2, 3, 4, 5], 500);
13
14  for await (const update of generator) {
15    console.log(`[${update.timestamp}] ${update.dinosaur.name} : santé ${update.dinosaur.health}%`);
16  }
17
18  console.log("Cycle de surveillance en direct terminé.");
19}
20
21monitorLive();

Erreurs courantes à éviter

1// Erreur 1 : utiliser await sans async
2function badFunction() {
3  const data = await fetchDinosaurData(1); // SyntaxError !
4}
5
6// Correction : marquer la fonction comme async
7async function goodFunction() {
8  const data = await fetchDinosaurData(1); // OK
9}
10
11// Erreur 2 : awaits séquentiels pour des opérations indépendantes (lent)
12async function slowReport(id) {
13  const a = await fetchDinosaurData(id);
14  const b = await fetchEnclosureStatus(a.enclosureId); // N'a besoin que de enclosureId
15  const c = await fetchVetReport(id); // Indépendant - mais attend que b se termine !
16  return { a, b, c };
17}
18
19// Correction : parallèle quand c'est possible
20async function fastReport(id) {
21  const dino = await fetchDinosaurData(id);
22  const [enclosure, vetReport] = await Promise.all([
23    fetchEnclosureStatus(dino.enclosureId),
24    fetchVetReport(id) // S'exécute en parallèle de la récupération de l'enclos
25  ]);
26  return { dino, enclosure, vetReport };
27}
28
29// Erreur 3 : oublier le await - fire and forget
30async function badSave(data) {
31  saveToDatabase(data); // Pas de await - les erreurs sont silencieusement avalées !
32  console.log("Enregistré !"); // Affiché avant la fin de l'enregistrement
33}
34
35// Correction : toujours await les opérations async dont le résultat compte
36async function goodSave(data) {
37  await saveToDatabase(data);
38  console.log("Enregistré !");
39}
40
41function saveToDatabase(data) { return new Promise(res => setTimeout(res, 500)); }

Patterns de conception avec async/await

La syntaxe async/await ouvre de nouvelles possibilités de patterns de conception en JavaScript. Voici quelques patterns utiles :

Pattern Retry

1async function withRetries(operation, options = {}) {
2  const { maxAttempts = 3, delay = 1000, backoffMultiplier = 2 } = options;
3
4  let lastError;
5
6  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
7    try {
8      if (attempt > 1) {
9        console.log(`Tentative ${attempt}/${maxAttempts}...`);
10      }
11
12      return await operation();
13    } catch (error) {
14      console.error(`Tentative ${attempt} échouée : ${error.message}`);
15      lastError = error;
16
17      if (attempt < maxAttempts) {
18        const waitTime = delay * Math.pow(backoffMultiplier, attempt - 1);
19        console.log(`Attente de ${waitTime}ms avant la prochaine tentative...`);
20        await new Promise(resolve => setTimeout(resolve, waitTime));
21      }
22    }
23  }
24
25  throw new Error(`Opération échouée après ${maxAttempts} tentatives. Dernière erreur : ${lastError.message}`);
26}
27
28// Exemple d'utilisation
29async function connectToSecuritySystem() {
30  return withRetries(
31    async () => {
32      const connection = await establishSecuritySystemConnection();
33      const authentication = await authenticateConnection(connection);
34      return await fetchSecurityStatus(connection, authentication);
35    },
36    {
37      maxAttempts: 5,
38      delay: 2000,
39      backoffMultiplier: 1.5
40    }
41  );
42}

Pattern Timeout

1async function withTimeout(operation, timeLimit, message) {
2  // Créer une Promise qui se rejette après la limite de temps
3  const timeoutPromise = new Promise((_, reject) => {
4    setTimeout(() => {
5      reject(new Error(`Limite de temps dépassée (${timeLimit}ms) : ${message}`));
6    }, timeLimit);
7  });
8
9  // Course entre l'opération et le timeout
10  return Promise.race([operation(), timeoutPromise]);
11}
12
13// Exemple d'utilisation
14async function safeTemperatureReading(enclosureId) {
15  try {
16    const temperature = await withTimeout(
17      () => readEnclosureTemperature(enclosureId),
18      5000,
19      `Lecture de la température de l'enclos n°${enclosureId}`
20    );
21
22    console.log(`Enclos n°${enclosureId} : ${temperature}°C`);
23    return temperature;
24  } catch (error) {
25    console.error(`Erreur de lecture de température : ${error.message}`);
26    return null; // Ou on peut retourner une valeur par défaut
27  }
28}

Retry avec Exponential Backoff

1async function withExponentialBackoff(operation, options = {}) {
2  const {
3    maxAttempts = 5,
4    baseWait = 1000,
5    maxWait = 30000,
6    jitter = true
7  } = options;
8
9  let attempt = 0;
10
11  while (true) {
12    try {
13      return await operation(++attempt);
14    } catch (error) {
15      if (attempt >= maxAttempts) {
16        throw new Error(`Opération échouée après ${attempt} tentatives : ${error.message}`);
17      }
18
19      // Calculer le temps d'attente avec exponential backoff
20      let waitTime = Math.min(
21        maxWait,
22        baseWait * Math.pow(2, attempt - 1)
23      );
24
25      // Ajouter du jitter (aléatoire) au temps d'attente
26      if (jitter) {
27        waitTime = waitTime * (0.5 + Math.random() * 0.5);
28      }
29
30      console.log(`Tentative ${attempt} échouée. Attente de ${Math.round(waitTime)}ms avant la prochaine tentative...`);
31
32      await new Promise(resolve => setTimeout(resolve, waitTime));
33    }
34  }
35}
36
37// Exemple d'utilisation
38async function synchronizeDataWithCentral() {
39  return withExponentialBackoff(
40    async (attempt) => {
41      console.log(`Tentative de synchronisation n°${attempt}...`);
42      const data = await fetchCurrentData();
43      await sendDataToCentral(data);
44      return `Synchronisation terminée : ${data.length} enregistrements`;
45    },
46    {
47      maxAttempts: 10,
48      baseWait: 2000,
49      maxWait: 60000
50    }
51  );
52}

Pattern Waterfall (opérations en cascade)

1async function cascadeSearch(query) {
2  console.log(`Démarrage de la recherche en cascade pour : ${query}`);
3
4  // Essayer d'abord le cache
5  try {
6    console.log("Recherche dans le cache...");
7    const cacheResult = await searchCache(query);
8    console.log("Trouvé dans le cache !");
9    return {
10      source: "cache",
11      result: cacheResult
12    };
13  } catch (error) {
14    console.log("Introuvable dans le cache, essai de la base de données locale...");
15  }
16
17  // Puis essayer la base de données locale
18  try {
19    const localResult = await searchLocalDatabase(query);
20    console.log("Trouvé dans la base de données locale !");
21    // Mettre à jour le cache
22    await updateCache(query, localResult);
23    return {
24      source: "local_db",
25      result: localResult
26    };
27  } catch (error) {
28    console.log("Introuvable dans la base locale, essai de la base de données centrale...");
29  }
30
31  // Enfin, essayer la base de données centrale
32  try {
33    const centralResult = await searchCentralDatabase(query);
34    console.log("Trouvé dans la base de données centrale !");
35    // Mettre à jour la base locale et le cache
36    await updateLocalDatabase(query, centralResult);
37    await updateCache(query, centralResult);
38    return {
39      source: "central_db",
40      result: centralResult
41    };
42  } catch (error) {
43    throw new Error(`Aucun résultat trouvé pour la requête : ${query}`);
44  }
45}

Résumé

La syntaxe async/await offre une façon élégante et lisible de travailler avec du code asynchrone en JavaScript, construite par-dessus les Promises. Voici les principaux avantages :

  1. Lisibilité - le code ressemble davantage à du code synchrone, ce qui facilite la compréhension du flux du programme
  2. Gestion des erreurs - possibilité d'utiliser la syntaxe standard try/catch au lieu de .catch()
  3. Débogage - suivi de la stack trace et placement des points d'arrêt plus faciles
  4. Composition - composition plus aisée d'opérations asynchrones complexes

Cependant, garde à l'esprit quelques points importants :

  1. Les fonctions async retournent toujours une Promise - même si tu n'utilises pas await à l'intérieur de la fonction
  2. Attention à l'exécution séquentielle - utiliser await avant chaque appel entraîne une exécution séquentielle, qui peut ne pas être optimale
  3. N'oublie pas la gestion des erreurs - utilise toujours try/catch ou .catch() lorsque tu appelles des fonctions async
  4. Utilise Promise.all() pour l'exécution parallèle - quand les opérations sont indépendantes et peuvent s'exécuter en parallèle

Au Parc Jurassique, comme dans toute application JavaScript complexe, async/await est un outil précieux pour gérer les opérations asynchrones, comme communiquer avec les systèmes de sécurité, surveiller les dinosaures ou coordonner les différents sous-systèmes du parc.

Dans le prochain exercice, nous nous concentrerons sur les stratégies de gestion des erreurs dans le code asynchrone, ce qui est crucial pour créer des applications résilientes et fiables.

Code de cette leçon : index.js
1// Closure Counter - Closures
2console.log("CLOSURE - DINOSAUR COUNTER\n");
3
4// Closure - a function that remembers its environment
5function createDinosaurCounter(species) {
6  let count = 0; // Variable privée
7
8  return {
9    increment: function() {
10      count++;
11      console.log(`${species} count: ${count}`);
12      return count;
13    },
14
15    decrement: function() {
16      if (count > 0) {
17        count--;
18        console.log(`${species} count: ${count}`);
19      } else {
20        console.log(`${species} count is already 0!`);
21      }
22      return count;
23    },
24
25    getCount: function() {
26      return count;
27    },
28
29    reset: function() {
30      count = 0;
31      console.log(`${species} counter réinitialisé`);
32    }
33  };
34}
35
36// Creating counters for different species
37console.log("=== CREATING COUNTERS ===\n");
38
39const trexCounter = createDinosaurCounter('T-Rex');
40const velociraptorCounter = createDinosaurCounter('Velociraptor');
41
42console.log("T-Rex:");
43trexCounter.increment(); // 1
44trexCounter.increment(); // 2
45trexCounter.increment(); // 3
46
47console.log("\nVelociraptor:");
48velociraptorCounter.increment(); // 1
49velociraptorCounter.increment(); // 2
50
51console.log("\nT-Rex à nouveau:");
52trexCounter.increment(); // 4
53
54console.log(`\n=== COUNTER STATE ===`);
55console.log(`T-Rex: ${trexCounter.getCount()}`);
56console.log(`Velociraptor: ${velociraptorCounter.getCount()}`);
57
58console.log("\n=== DÉCRÉMENTATION ===\n");
59trexCounter.decrement(); // 3
60velociraptorCounter.decrement(); // 1
61velociraptorCounter.decrement(); // 0
62velociraptorCounter.decrement(); // Attempt to go below 0
63
64console.log("\n=== RESET ===\n");
65trexCounter.reset();
66console.log(`T-Rex après reset: ${trexCounter.getCount()}`);
67
68// count is inaccessible from outside!
69// console.log(count); // ERROR! count est privé

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 le mot-clé await ?

  2. 2. Que retourne automatiquement une fonction marquée du mot-clé async ?

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

Tâches pratiques dans le jeu

  • Éditeur de code

    Écris une fonction async qui utilise await pour vérifier successivement trois capteurs de dinosaures et afficher les résultats.

  • Tri horizontal

    Ordonne les éléments de l'exécution parallèle de Promises :

Articles utiles