Tutoriel

Les Promesses et async/await en JavaScript

Introduction aux opérations asynchrones

En JavaScript, les opérations asynchrones sont essentielles pour éviter de bloquer le thread principal lors d'opérations qui prennent du temps, comme les appels API, la lecture de fichiers, ou les requêtes réseau.

Le JavaScript est un langage à thread unique, ce qui signifie qu'il ne peut exécuter qu'une seule chose à la fois. Sans l'asynchrone, toute opération longue bloquerait l'interface utilisateur, rendant l'application non responsive.

Les opérations asynchrones courantes incluent :

  • Les appels API (fetch, XMLHttpRequest)
  • La lecture/écriture de fichiers
  • Les timers (setTimeout, setInterval)
  • Les requêtes de base de données
  • Les opérations réseau
console.log("1");
console.log("2");
console.log("3");
// Résultat : 1, 2, 3 (dans l'ordre)

// Exemple d'opération asynchrone (non bloquante)
console.log("1");
setTimeout(() => console.log("2"), 1000);
console.log("3");
// Résultat : 1, 3, 2 (le 2 apparaît après 1 seconde)

// Exemple avec fetch (asynchrone)
console.log("Début");
fetch("https://api.example.com/data")
  .then(() => console.log("Données reçues"));
console.log("Fin");
// Résultat : Début, Fin, Données reçues

Les callbacks (approche traditionnelle)

Les callbacks sont des fonctions passées en argument à une autre fonction, qui seront exécutées une fois l'opération asynchrone terminée.

C'est l'approche traditionnelle pour gérer l'asynchrone en JavaScript. Cependant, elle peut rapidement mener au "callback hell" (pyramide de callbacks) lorsque plusieurs opérations asynchrones doivent être chaînées.

Le problème principal des callbacks est la difficulté de lecture et de maintenance du code lorsqu'on a plusieurs niveaux d'imbrication.

function chargerDonnees(callback) {
  setTimeout(() => {
    callback("Données chargées");
  }, 1000);
}

chargerDonnees((donnees) => {
  console.log(donnees); // "Données chargées" après 1 seconde
});

// Exemple de "callback hell" (pyramide de callbacks)
chargerUtilisateur((utilisateur) => {
  chargerProfil(utilisateur.id, (profil) => {
    chargerCommandes(profil.id, (commandes) => {
      chargerDetails(commandes[0].id, (details) => {
        console.log(details); // 4 niveaux d'imbrication !
      });
    });
  });
});

// Ce code devient rapidement difficile à lire et maintenir

Les Promesses (Promises)

Les Promesses (Promises) ont été introduites dans ES6 pour résoudre les problèmes des callbacks. Une Promesse représente une valeur qui sera disponible dans le futur, soit avec succès (résolue), soit avec une erreur (rejetée).

Une Promesse peut être dans trois états :

  • Pending (en attente) : état initial
  • Fulfilled (résolue) : opération réussie
  • Rejected (rejetée) : opération échouée

Les Promesses permettent de chaîner les opérations asynchrones de manière plus lisible avec

.then()
et de gérer les erreurs avec
.catch()
.

const maPromesse = new Promise((resolve, reject) => {
  const succes = true;
  
  if (succes) {
    resolve("Opération réussie !");
  } else {
    reject("Erreur !");
  }
});

// Utilisation avec .then() et .catch()
maPromesse
  .then((resultat) => {
    console.log(resultat); // "Opération réussie !"
    return "Nouvelle valeur";
  })
  .then((nouvelleValeur) => {
    console.log(nouvelleValeur); // "Nouvelle valeur"
  })
  .catch((erreur) => {
    console.error(erreur); // En cas d'erreur
  });

// Exemple avec fetch() (retourne une Promesse)
fetch("https://api.example.com/users")
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
    return fetch(`https://api.example.com/users/${data[0].id}`);
  })
  .then((response) => response.json())
  .then((user) => console.log(user))
  .catch((error) => console.error("Erreur:", error));

// Chaînage de promesses (plus lisible que les callbacks)
chargerUtilisateur()
  .then((utilisateur) => chargerProfil(utilisateur.id))
  .then((profil) => chargerCommandes(profil.id))
  .then((commandes) => chargerDetails(commandes[0].id))
  .then((details) => console.log(details))
  .catch((error) => console.error(error));

async/await (syntaxe moderne)

async/await est une syntaxe introduite dans ES2017 qui rend le code asynchrone encore plus lisible et similaire au code synchrone. C'est essentiellement du "sucre syntaxique" sur les Promesses.

Pour utiliser async/await :

  • Déclarez une fonction avec le mot-clé
    async
  • Utilisez
    await
    devant les opérations asynchrones (qui retournent une Promesse)
  • Le code s'exécute de manière séquentielle, mais reste non bloquant

Les avantages d'async/await :

  • Code plus lisible et plus facile à comprendre
  • Gestion d'erreurs simplifiée avec try/catch
  • Style de code plus proche du code synchrone
  • Meilleure stack trace en cas d'erreur
async function chargerDonnees() {
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  return data;
}

// Utilisation
chargerDonnees()
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

// Fonction async avec await (plus lisible)
async function chargerUtilisateurComplet() {
  const utilisateur = await chargerUtilisateur();
  const profil = await chargerProfil(utilisateur.id);
  const commandes = await chargerCommandes(profil.id);
  const details = await chargerDetails(commandes[0].id);
  return details;
}

// Comparaison : Promesses vs async/await

// Avec Promesses (.then())
function exemplePromesses() {
  fetch("https://api.example.com/data")
    .then((response) => response.json())
    .then((data) => {
      console.log(data);
      return fetch(`https://api.example.com/data/${data.id}`);
    })
    .then((response) => response.json())
    .then((details) => console.log(details))
    .catch((error) => console.error(error));
}

// Avec async/await (plus lisible)
async function exempleAsyncAwait() {
  try {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    console.log(data);
    
    const response2 = await fetch(`https://api.example.com/data/${data.id}`);
    const details = await response2.json();
    console.log(details);
  } catch (error) {
    console.error(error);
  }
}

// Les deux font la même chose, mais async/await est plus lisible !

Gestion d'erreurs avec try/catch

La gestion d'erreurs est cruciale en programmation asynchrone. Avec les Promesses, on utilise

.catch()
, mais avec async/await, on peut utiliser le bloc
try/catch
classique, ce qui rend la gestion d'erreurs plus intuitive et cohérente avec le reste du code JavaScript.

Points importants :

  • Les erreurs non capturées dans une Promesse peuvent être silencieuses
  • try/catch
    avec async/await capture les erreurs de manière synchrone
  • On peut combiner plusieurs opérations dans un seul bloc try/catch
  • Il est important de toujours gérer les erreurs pour éviter les crashes
fetch("https://api.example.com/data")
  .then((response) => {
    if (!response.ok) {
      throw new Error("Erreur HTTP: " + response.status);
    }
    return response.json();
  })
  .then((data) => console.log(data))
  .catch((error) => {
    console.error("Erreur:", error.message);
    // Gestion de l'erreur
  });

// Gestion d'erreurs avec async/await (try/catch)
async function chargerDonneesAvecErreur() {
  try {
    const response = await fetch("https://api.example.com/data");
    
    if (!response.ok) {
      throw new Error("Erreur HTTP: " + response.status);
    }
    
    const data = await response.json();
    console.log(data);
    return data;
  } catch (error) {
    console.error("Erreur lors du chargement:", error.message);
    // Gestion de l'erreur (affichage message, fallback, etc.)
    return null; // Valeur par défaut
  }
}

// Exemple avec plusieurs opérations
async function processusComplet() {
  try {
    const user = await chargerUtilisateur();
    const profil = await chargerProfil(user.id);
    const commandes = await chargerCommandes(profil.id);
    
    // Si une erreur survient à n'importe quelle étape,
    // elle sera capturée par le catch
    return { user, profil, commandes };
  } catch (error) {
    console.error("Erreur dans le processus:", error);
    // Toutes les erreurs sont gérées au même endroit
    throw error; // Re-lancer l'erreur si nécessaire
  }
}

// Erreurs courantes à éviter
async function exempleErreurs() {
  // ❌ Mauvaise pratique : pas de gestion d'erreur
  const data = await fetch("https://api.example.com/data");
  
  // ✅ Bonne pratique : avec try/catch
  try {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Erreur:", error);
    return null;
  }
}

// Gestion d'erreurs spécifiques
async function gestionErreursSpecifiques() {
  try {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    return data;
  } catch (error) {
    if (error instanceof TypeError) {
      console.error("Erreur de réseau:", error);
    } else if (error instanceof SyntaxError) {
      console.error("Erreur de parsing JSON:", error);
    } else {
      console.error("Erreur inconnue:", error);
    }
    throw error;
  }
}

Comparaison et bonnes pratiques

Maintenant que vous connaissez les trois approches, voici un récapitulatif et des bonnes pratiques :

Callbacks : Ancienne approche, à éviter pour les nouvelles opérations asynchrones complexes.

Promesses : Excellente approche, toujours pertinente, surtout pour les opérations simples ou quand vous voulez chaîner avec

.then()
.

async/await : Syntaxe moderne recommandée pour la plupart des cas, surtout pour le code complexe avec plusieurs opérations asynchrones.

Bonnes pratiques :

  • Toujours gérer les erreurs (try/catch ou .catch())
  • Utiliser async/await pour améliorer la lisibilité
  • Éviter de mélanger les approches dans le même bloc de code
  • Utiliser Promise.all() pour exécuter plusieurs promesses en parallèle
  • Ne pas oublier le mot-clé await dans les fonctions async

// 1. CALLBACKS (ancienne approche)
function avecCallbacks() {
  chargerDonnees((data) => {
    traiterDonnees(data, (resultat) => {
      sauvegarder(resultat, (succes) => {
        console.log("Terminé");
      });
    });
  });
}

// 2. PROMESSES (approche moderne)
function avecPromesses() {
  chargerDonnees()
    .then((data) => traiterDonnees(data))
    .then((resultat) => sauvegarder(resultat))
    .then(() => console.log("Terminé"))
    .catch((error) => console.error(error));
}

// 3. ASYNC/AWAIT (syntaxe moderne recommandée)
async function avecAsyncAwait() {
  try {
    const data = await chargerDonnees();
    const resultat = await traiterDonnees(data);
    await sauvegarder(resultat);
    console.log("Terminé");
  } catch (error) {
    console.error(error);
  }
}

// Exécution en parallèle avec Promise.all()
async function executionParallele() {
  try {
    // Exécute plusieurs promesses en parallèle (plus rapide)
    const [users, posts, comments] = await Promise.all([
      fetch("/api/users").then(r => r.json()),
      fetch("/api/posts").then(r => r.json()),
      fetch("/api/comments").then(r => r.json())
    ]);
    
    console.log(users, posts, comments);
  } catch (error) {
    console.error("Une des requêtes a échoué:", error);
  }
}

// Exemple complet combinant tous les concepts
async function exempleComplet() {
  try {
    // 1. Charger les données utilisateur
    const userResponse = await fetch("https://api.example.com/user/1");
    if (!userResponse.ok) throw new Error("Utilisateur non trouvé");
    const user = await userResponse.json();
    
    // 2. Charger le profil en parallèle avec les commandes
    const [profilResponse, commandesResponse] = await Promise.all([
      fetch(`https://api.example.com/profil/${user.id}`),
      fetch(`https://api.example.com/commandes/${user.id}`)
    ]);
    
    const profil = await profilResponse.json();
    const commandes = await commandesResponse.json();
    
    // 3. Traiter les données
    const resultat = {
      utilisateur: user,
      profil: profil,
      nombreCommandes: commandes.length
    };
    
    return resultat;
  } catch (error) {
    console.error("Erreur dans exempleComplet:", error);
    // Gestion d'erreur appropriée
    return null;
  }
}

// Pièges à éviter
async function piegesAEviter() {
  // ❌ Oubli du await
  const data = fetch("/api/data"); // data est une Promesse, pas les données !
  
  // ✅ Correct
  const data = await fetch("/api/data");
  
  // ❌ Pas de gestion d'erreur
  const result = await operationRisquee();
  
  // ✅ Correct
  try {
    const result = await operationRisquee();
  } catch (error) {
    console.error(error);
  }
  
  // ❌ Boucle avec await (séquentiel au lieu de parallèle)
  for (const id of ids) {
    await chargerDonnee(id); // Lent !
  }
  
  // ✅ Correct (parallèle)
  await Promise.all(ids.map(id => chargerDonnee(id)));
}

Section commentaire