Comprendre Async/Await et les Promises en JavaScript : Le Guide Visuel

10 min de lecture JavaScript

Tu as sûrement déjà vécu ce moment : tu écris du JavaScript, tout semble logique, tu lances ton code... et les console.log s'affichent dans un ordre qui défie toute logique. Bienvenue dans le monde de l'asynchrone en JavaScript. C'est probablement le concept qui fait trébucher le plus de développeurs, débutants comme intermédiaires. Si tu débutes, jette d'abord un oeil à les erreurs classiques en JavaScript pour solidifier tes bases.Et pourtant, comprendre les Promises en JavaScript et maîtriser async/await n'est pas sorcier. Le problème, c'est que la plupart des tutoriels te balancent la théorie sans te montrer pourquoi ça fonctionne comme ça. Résultat : tu copies du code sans vraiment le comprendre, et dès que ça casse, tu es perdu.Dans ce guide, on va déconstruire l'asynchrone en JavaScript pièce par pièce. On part des callbacks, on passe par les Promises, on arrive à async/await, et on termine par le event loop JavaScript — le chef d'orchestre invisible qui gère tout ça. Avec des exemples concrets, des erreurs courantes à éviter, et des cas pratiques que tu pourras réutiliser dans tes projets dès aujourd'hui.## Pourquoi JavaScript est asynchroneJavaScript est single-threaded. Ça veut dire qu'il n'a qu'un seul fil d'exécution : il ne peut faire qu'une seule chose à la fois. Contrairement à des langages comme Java ou Go qui peuvent lancer plusieurs threads en parallèle, JavaScript traite les instructions une par une, dans l'ordre.Alors pourquoi est-ce qu'on parle de javascript asynchrone ? Parce que si JavaScript devait attendre chaque opération lente (requête réseau, lecture de fichier, timer...) avant de passer à la suite, ton application serait complètement gelée. Imagine : tu cliques sur un bouton qui fait un appel API, et pendant 2 secondes, plus rien ne répond. L'horreur.La solution : déléguer les opérations lentes à l'environnement (le navigateur ou Node.js), et continuer à exécuter le reste du code. Quand l'opération est terminée, JavaScript est prévenu et exécute le code de retour. C'est le principe fondamental de l'asynchrone.``javascriptconsole.log("Début");setTimeout(() => { console.log("Timeout terminé");}, 1000);console.log("Fin");// Affiche :// Début// Fin// Timeout terminé`"Fin" s'affiche avant "Timeout terminé" parce que setTimeout est délégué au navigateur. JavaScript n'attend pas — il continue.## Les Callbacks : le début de l'histoireAvant les Promises, la seule façon de gérer l'asynchrone était les callbacks : tu passes une fonction en paramètre, et elle sera appelée quand l'opération est terminée.`javascriptfunction recupererUtilisateur(id, callback) { setTimeout(() => { callback({ id: id, nom: "Alice" }); }, 500);}recupererUtilisateur(1, (utilisateur) => { console.log(utilisateur.nom); // "Alice"});`Simple et efficace pour un seul appel. Mais que se passe-t-il quand tu enchaînes plusieurs opérations asynchrones ?C'est là qu'on tombe dans ce qu'on appelle le callback hell — ou la pyramide infernale :`javascript// ❌ Callback hellrecupererUtilisateur(1, (utilisateur) => { recupererCommandes(utilisateur.id, (commandes) => { recupererDetails(commandes[0].id, (details) => { recupererAvis(details.produitId, (avis) => { afficher(avis); }); }); });});`Chaque niveau d'imbrication rend le code plus difficile à lire, à débugger et à maintenir. Et on n'a même pas géré les erreurs ici. Le callback hell en JavaScript, c'est exactement ça : un empilement de fonctions imbriquées qui devient vite ingérable.Avec la gestion d'erreurs, c'est encore pire :`javascript// ❌ Callback hell avec gestion d'erreursrecupererUtilisateur(1, (erreur, utilisateur) => { if (erreur) { console.error("Erreur utilisateur:", erreur); return; } recupererCommandes(utilisateur.id, (erreur, commandes) => { if (erreur) { console.error("Erreur commandes:", erreur); return; } recupererDetails(commandes[0].id, (erreur, details) => { if (erreur) { console.error("Erreur details:", erreur); return; } afficher(details); }); });});`Chaque callback doit vérifier l'erreur individuellement. Le code est devenu un cauchemar. Il fallait une meilleure solution.## Les Promises : la solution éléganteLes Promises (promesses en français) ont été introduites pour résoudre exactement ce problème. Une promesse JavaScript représente une valeur qui n'existe pas encore mais qui existera dans le futur.Une Promise a trois états :- pending (en attente) : l'opération est en cours- fulfilled (résolue) : l'opération a réussi, la valeur est disponible- rejected (rejetée) : l'opération a échoué, une erreur est disponible`javascriptconst maPromesse = new Promise((resolve, reject) => { const succes = true; setTimeout(() => { if (succes) { resolve("Données récupérées !"); } else { reject("Erreur de connexion"); } }, 1000);});maPromesse .then((resultat) => console.log(resultat)) .catch((erreur) => console.error(erreur));`resolve transmet la valeur de succès à .then(), et reject transmet l'erreur à .catch(). C'est une promesse : "je te donnerai le résultat plus tard".Le vrai pouvoir des Promises, c'est le chaînage. Chaque .then() retourne une nouvelle Promise, ce qui permet d'enchaîner les opérations de manière plate et lisible :`javascript// ✅ Chaînage de Promises — adieu callback hellrecupererUtilisateur(1) .then((utilisateur) => recupererCommandes(utilisateur.id)) .then((commandes) => recupererDetails(commandes[0].id)) .then((details) => recupererAvis(details.produitId)) .then((avis) => afficher(avis)) .catch((erreur) => console.error("Erreur:", erreur));`Même logique que le callback hell, mais en version plate et lisible. Et un seul .catch() attrape toutes les erreurs de la chaîne.Pour transformer une fonction à callback en Promise :`javascriptfunction recupererUtilisateur(id) { return new Promise((resolve, reject) => { setTimeout(() => { if (id > 0) { resolve({ id: id, nom: "Alice" }); } else { reject("ID invalide"); } }, 500); });}`## Async/Await : le sucre syntaxique magiqueAsync/await, introduit en ES2017, est du sucre syntaxique au-dessus des Promises. Ça ne change rien au fonctionnement interne — c'est toujours des Promises en dessous — mais ça rend le code asynchrone aussi lisible que du code synchrone.`javascript// ✅ Async/await — lisible comme du code synchroneasync function afficherAvis() { try { const utilisateur = await recupererUtilisateur(1); const commandes = await recupererCommandes(utilisateur.id); const details = await recupererDetails(commandes[0].id); const avis = await recupererAvis(details.produitId); afficher(avis); } catch (erreur) { console.error("Erreur:", erreur); }}afficherAvis();`C'est exactement le même code que le chaînage de Promises, mais écrit de manière séquentielle. await met en pause la fonction jusqu'à ce que la Promise soit résolue, puis retourne la valeur.Deux règles essentielles :- await ne peut être utilisé que dans une fonction async (et attention aux déclarations de variables — pour bien comprendre les variables JavaScript, c'est un prérequis)- Une fonction async retourne toujours une Promise`javascriptasync function saluer() { return "Bonjour";}// C'est équivalent à :function saluer() { return Promise.resolve("Bonjour");}saluer().then(msg => console.log(msg)); // "Bonjour"`## Le Event Loop expliqué simplementLe event loop JavaScript est le mécanisme qui coordonne l'exécution du code, les callbacks et les événements. C'est lui qui décide quand chaque morceau de code s'exécute.Voici le modèle simplifié :1. Call Stack : la pile d'exécution. Le code synchrone s'exécute ici.2. Web APIs : le navigateur gère les timers, requêtes HTTP, etc. en dehors du call stack.3. Callback Queue (macrotasks) : file d'attente pour setTimeout, setInterval, événements DOM.4. Microtask Queue : file d'attente prioritaire pour les .then() de Promises et queueMicrotask.5. Event Loop : vérifie en boucle si le call stack est vide. Si oui, il prend d'abord toutes les microtasks, puis une macrotask.Testons avec le puzzle classique :`javascriptconsole.log("1");setTimeout(() => { console.log("2");}, 0);Promise.resolve().then(() => { console.log("3");});console.log("4");// Affiche : 1, 4, 3, 2`Pourquoi cet ordre ? "1" et "4" sont synchrones (call stack). Ensuite, "3" est une microtask (Promise) et passe avant "2" qui est une macrotask (setTimeout). Les microtasks ont toujours la priorité, même avec un setTimeout à 0ms.Un exemple plus poussé :`javascriptconsole.log("A");setTimeout(() => console.log("B"), 0);Promise.resolve() .then(() => { console.log("C"); return Promise.resolve(); }) .then(() => console.log("D"));queueMicrotask(() => console.log("E"));console.log("F");// Affiche : A, F, C, E, D, B`A et F sont synchrones. Puis les microtasks : C, E, D. Enfin B, la macrotask.## Erreurs courantes avec l'async### Erreur 1 : Oublier await```javascript// ❌ Oublier awaitasync function getUser() { const reponse = fetch("/api/user"); const data = reponse.json(); // TypeError: reponse.json is not a function}````javascript// ✅ Avec awaitasync function getUser() { const reponse = await fetch("/api/user"); const data = await reponse.json(); return data;}`Sans await, reponse est une Promise, pas un objet Response.### Erreur 2 : Séquentiel inutile dans une boucle`javascript// ❌ Chaque requête attend la précédente — lentasync function recupererTous(ids) { const resultats = []; for (const id of ids) { const data = await fetch(/api/items/${id}); resultats.push(await data.json()); } return resultats; // 5 × 200ms = 1000ms}````javascript// ✅ Parallèle avec Promise.all — rapideasync function recupererTous(ids) { const promises = ids.map((id) => fetch(/api/items/${id}).then((r) => r.json()) ); return Promise.all(promises); // ~200ms total}`### Erreur 3 : Ne pas gérer les erreurs`javascript// ❌ Pas de gestion d'erreurasync function chargerDonnees() { const reponse = await fetch("/api/data"); const data = await reponse.json(); return data;}````javascript// ✅ Gestion complèteasync function chargerDonnees() { try { const reponse = await fetch("/api/data"); if (!reponse.ok) { throw new Error(HTTP ${reponse.status}); } return await reponse.json(); } catch (erreur) { console.error("Échec:", erreur.message); return null; }}`fetch ne rejette pas sur un 404 ou 500 — uniquement sur une erreur réseau. Vérifie reponse.ok.### Erreur 4 : Mélanger .then() et await`javascript// ❌ Mélange confusasync function getData() { const data = await fetch("/api/data") .then((r) => r.json()) .then((json) => json.results); return data;}````javascript// ✅ Tout en await — cohérentasync function getData() { const reponse = await fetch("/api/data"); const json = await reponse.json(); return json.results;}`### Erreur 5 : Promise inutile dans une fonction async`javascript// ❌ Promise redondanteasync function getUser(id) { return new Promise((resolve) => { const data = fetch(/api/users/${id}); resolve(data); });}````javascript// ✅ Directasync function getUser(id) { const reponse = await fetch(/api/users/${id}); return reponse.json();}`## Cas pratiques### Fetch API avec gestion d'erreur`javascriptasync function rechercherArticles(motCle) { try { const reponse = await fetch( https://api.exemple.com/articles?q=${encodeURIComponent(motCle)} ); if (!reponse.ok) throw new Error(Erreur ${reponse.status}); const { articles, total } = await reponse.json(); console.log(${total} articles trouvés); return articles; } catch (erreur) { console.error("Erreur:", erreur.message); return []; }}`### Requêtes parallèles avec Promise.all`javascriptasync function chargerTableauDeBord(userId) { const [profil, notifications, stats] = await Promise.all([ fetch(/api/users/${userId}).then((r) => r.json()), fetch(/api/users/${userId}/notifications).then((r) => r.json()), fetch(/api/users/${userId}/stats).then((r) => r.json()), ]); return { profil, notifications, stats };}`### Promise.allSettled pour la tolérance aux erreurs`javascriptasync function chargerAvecTolerance(userId) { const resultats = await Promise.allSettled([ fetch(/api/users/${userId}).then((r) => r.json()), fetch(/api/users/${userId}/notifications).then((r) => r.json()), fetch(/api/users/${userId}/stats).then((r) => r.json()), ]); return { profil: resultats[0].status === "fulfilled" ? resultats[0].value : null, notifications: resultats[1].status === "fulfilled" ? resultats[1].value : [], stats: resultats[2].status === "fulfilled" ? resultats[2].value : {}, };}`### Séquentiel vs Parallèle`javascript// Séquentiel : quand chaque étape dépend de la précédenteasync function passerCommande(panier) { const paiement = await validerPaiement(panier.total); const commande = await creerCommande(panier, paiement.id); return await envoyerEmail(commande.id);}// Parallèle : quand les opérations sont indépendantesasync function initialiserApp() { const [config, traductions, user] = await Promise.all([ chargerConfig(), chargerTraductions("fr"), recupererSession(), ]); return { config, traductions, user };}`## Tableau récapitulatif

ConceptSyntaxeGestion d'erreurLisibilitéCas d'usage
Callbacksfn(arg, callback)ManuelleFaible (imbrication)Événements DOM, legacy
Promises.then().catch().catch() en chaîneMoyenneComposition, chaînage
Async/Awaitawait fn()try/catchExcellenteFlux complexes
Promise.allPromise.all([...])Fail-fastBonneRequêtes parallèles
Promise.allSettledPromise.allSettled([...])TolérantBonneChargement partiel

ConclusionL'asynchrone en JavaScript n'est pas magique — c'est un système logique une fois que tu comprends ses briques : le event loop orchestre tout, les callbacks sont la base historique, les Promises apportent la structure, et async/await rend le tout lisible.Retiens ces principes :- JavaScript est single-threaded mais délègue les opérations lentes- Les microtasks (Promises) passent toujours avant les macrotasks (setTimeout)- Utilise await pour le séquentiel, Promise.all pour le parallèle- Gère toujours tes erreurs avec try/catch ou .catch()`- Ne mélange pas les styles — reste cohérent- Consulte la doc officielle — la documentation MDN sur async/await est la référence incontournableUne fois l'asynchrone maîtrisé, tu peux débuter avec React où ces concepts sont omniprésents — chaque appel API dans un composant React utilise exactement les patterns que tu viens d'apprendre.Envie de visualiser tout ça en temps réel ? GoGoKodo propose un atelier Async Tracer qui anime le event loop, les callbacks et les Promises étape par étape — 100% gratuit.