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().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 devant les opérations asynchrones (qui retournent une Promesse)
await - 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()try/catchPoints importants :
- Les erreurs non capturées dans une Promesse peuvent être silencieuses
- avec async/await capture les erreurs de manière synchrone
try/catch - 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))); }