Aller au contenu principal
Lance-toi : 40% de réduction sur toutes les formations jusqu'au 30 juin En savoir plus

Asynchrone JavaScript : sortir du brouillard des callbacks, Promises et async/await

L'asynchrone en JavaScript est le concept qui bloque le plus les débutants. Callbacks, Promises, async/await, ce guide démêle tout avec des exemples concrets et explique pourquoi ça marche comme ça.

Guides & tutoriels · ·
Adel LATIBI
Adel LATIBI
Asynchrone JavaScript : sortir du brouillard des callbacks, Promises et async/await

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

Tu suis un cours JavaScript depuis quelques semaines. Les variables, les fonctions, les conditions, les boucles : tout passe. Et puis arrive un chapitre qui s'appelle "asynchrone", et soudain tu ne comprends plus rien. Le mot callback apparaît, suivi de Promise, suivi de async/await, et chaque exemple semble écrit dans une langue que personne ne parle dans la vraie vie.

Tu n'es pas seul à bloquer là. Je vois ça chez la plupart des gens en reconversion, chez les juniors qui débutent en entreprise, et même chez des curieux qui suivent des tutoriels sans contexte. Le code marche parfois, casse souvent, et personne ne sait pourquoi.

Le problème n'est pas que JavaScript serait mal conçu. Le problème, c'est que les ressources expliquent la syntaxe avant d'expliquer le mécanisme. Tu apprends à écrire await fetch(...) avant de savoir ce qu'est une Promise et pourquoi elle existe.

Cet article remet les choses dans l'ordre. À la fin, tu sauras pourquoi JavaScript a besoin d'être asynchrone, dans quel ordre les choses s'exécutent, et tu identifieras les pièges qui bouffent les performances de ton premier projet sérieux.

Le problème que l'asynchrone résout

Imagine une page web qui appelle une API pour afficher la météo. La requête met 800 millisecondes à revenir. Pendant ce temps, qu'est-ce que ton navigateur devrait faire ?

Si JavaScript était synchrone, la réponse serait : rien. Le bouton ne réagirait plus, le scroll serait bloqué, et l'utilisateur croirait que sa page a planté. Multiplie ça par toutes les requêtes, lectures de fichiers et timers d'une application moderne, et tu obtiens une expérience inutilisable.

JavaScript ne fait qu'une seule chose à la fois. Il a un seul thread d'exécution. C'est sa contrainte fondamentale, et elle ne changera pas. L'asynchrone est la solution à cette contrainte : on lance une opération longue, on continue à exécuter le reste du code, et on récupère le résultat plus tard.

Ce mécanisme s'appelle l'event loop. Tu n'as pas besoin de le maîtriser dans les moindres détails pour écrire du code qui fonctionne, mais tu dois savoir qu'il existe. C'est lui qui décide dans quel ordre les morceaux de ton code s'exécutent.

Un exemple suffit pour comprendre l'effet pratique. Si tu écris setTimeout(fn, 0), ta fonction ne sera pas exécutée immédiatement. Elle sera mise en file d'attente et appelée après que tout le code synchrone restant ait fini. C'est exactement ce qui te permet de faire setTimeout(fn, 0) pour reporter une opération coûteuse à la prochaine itération du moteur, sans bloquer l'interface.

Trois générations de syntaxe pour le même problème

JavaScript a connu trois manières d'écrire du code asynchrone. Chacune répond aux limites de la précédente. Pour comprendre async/await, il faut avoir vu les deux générations qui l'ont précédée.

Génération 1 : les callbacks

Un callback est une fonction qu'on passe en paramètre à une autre fonction, et qui sera appelée quand le travail sera terminé.

console.log("Avant le timer");

setTimeout(function() {
  console.log("Cette ligne s'affiche après 2 secondes");
}, 2000);

console.log("Après le timer");

// Ordre d'affichage :
// "Avant le timer"
// "Après le timer"
// "Cette ligne s'affiche après 2 secondes"

Les callbacks fonctionnent. Le problème arrive quand tu dois enchaîner plusieurs opérations asynchrones. Chaque résultat sert d'entrée à l'opération suivante, et tu te retrouves avec ça :

getUser(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      sendEmail(details, function(result) {
        // Bienvenue dans le callback hell
      });
    });
  });
});

Le code part en escalier vers la droite. Gérer les erreurs à chaque niveau devient un cauchemar. C'est ce qu'on appelle le callback hell, et c'est une vraie raison pour laquelle des gens abandonnent JavaScript.

Génération 2 : les Promises

Une Promise est un objet qui représente le résultat futur d'une opération. Au moment où tu la crées, tu ne connais pas encore la valeur. Elle peut prendre trois états : en attente (pending), résolue avec une valeur (fulfilled), ou rejetée avec une erreur (rejected).

Le grand apport des Promises, c'est le chaînage. Au lieu d'imbriquer, tu enchaînes horizontalement avec .then() :

fetch('https://api.example.com/users/42')
  .then(response => response.json())
  .then(user => fetch(`/api/orders?userId=${user.id}`))
  .then(response => response.json())
  .then(orders => {
    console.log(orders);
  })
  .catch(error => {
    console.error("Quelque chose a échoué :", error);
  });

Le .catch() à la fin attrape n'importe quelle erreur survenue dans la chaîne. Plus besoin de gérer les erreurs à chaque étape.

Génération 3 : async/await

La syntaxe async/await n'apporte rien de nouveau au moteur. C'est du sucre syntaxique posé par-dessus les Promises. Mais ce sucre rend le code lisible comme s'il était synchrone :

async function recupererCommandes(userId) {
  try {
    const reponseUser = await fetch(`/api/users/${userId}`);
    const user = await reponseUser.json();

    const reponseOrders = await fetch(`/api/orders?userId=${user.id}`);
    const orders = await reponseOrders.json();

    return orders;
  } catch (error) {
    console.error("Quelque chose a échoué :", error);
    throw error;
  }
}

Deux mots-clés à retenir. Le async placé devant une fonction signifie que cette fonction renverra toujours une Promise, même si tu retournes une valeur primitive comme un nombre. Le await ne peut être utilisé que dans une fonction async (ou au plus haut niveau d'un module ES). Il met la fonction en pause jusqu'à ce que la Promise soit résolue, puis renvoie sa valeur.

La gestion des erreurs se fait avec try/catch, comme dans du code synchrone classique. C'est cette uniformité qui a rendu le code asynchrone enfin abordable pour les juniors.

Un cas pratique complet

Voici un composant React typique qui charge un profil utilisateur depuis une API. Il gère trois états : chargement, succès, erreur. C'est exactement le genre de code que tu écriras au début d'une mission ou d'un projet personnel.

import { useState, useEffect } from 'react';

function ProfilUtilisateur({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function chargerUtilisateur() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);

        if (!response.ok) {
          throw new Error(`Statut HTTP : ${response.status}`);
        }

        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    chargerUtilisateur();
  }, [userId]);

  if (loading) return <p>Chargement...</p>;
  if (error) return <p>Erreur : {error}</p>;
  return <div>{user.name}</div>;
}

Trois choses méritent l'attention dans ce code. Le try/catch entoure les deux await, donc une erreur réseau ou un JSON invalide remontera au même endroit. Le test response.ok est obligatoire car fetch ne lève pas d'erreur sur un 404 ou un 500, il faut le vérifier soi-même. Le finally garantit que setLoading(false) sera exécuté quoi qu'il arrive.

Si tu débutes en React, ce pattern reviendra des dizaines de fois dans tes projets. La formation React de LaPolaris consacre un module entier à la gestion des effets asynchrones avec useEffect, parce que c'est l'endroit où le plus grand nombre de bugs apparaissent.

Les pièges qui coûtent cher

Piège 1 : oublier le mot await

Sans await, ta variable contient une Promise au lieu de la valeur attendue. Le code ne plante pas tout de suite, il plante plus loin avec un message qui n'a aucun rapport.

// Faux : data est une Promise, pas un objet
async function recupererUser() {
  const data = fetch('/api/user').json();
  console.log(data.name); // undefined
}

// Correct
async function recupererUser() {
  const response = await fetch('/api/user');
  const data = await response.json();
  console.log(data.name);
}

Piège 2 : await dans une boucle séquentielle

C'est l'erreur la plus coûteuse en performance. Quand tu mets await dans une boucle for...of, chaque itération attend la précédente.

const userIds = [1, 2, 3, 4, 5];

// Lent : 5 requêtes de 200 ms en série = 1 seconde
for (const id of userIds) {
  const user = await getUser(id);
  console.log(user);
}

// Rapide : 5 requêtes en parallèle = 200 ms
const users = await Promise.all(
  userIds.map(id => getUser(id))
);

Promise.all reçoit un tableau de Promises et renvoie une Promise qui se résout quand toutes ont terminé. Si l'une d'elles échoue, tout l'ensemble est rejeté. Pour un comportement plus tolérant, regarde Promise.allSettled.

Piège 3 : croire que fetch lève une erreur sur un 404

Beaucoup de juniors écrivent un try/catch autour de fetch en pensant que ça suffit. Faux. fetch ne rejette la Promise que si la requête réseau échoue (pas de connexion, DNS cassé). Une réponse HTTP 404 ou 500 est considérée comme un succès du point de vue réseau.

async function recupererDonnees() {
  try {
    const response = await fetch('/api/donnees');

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Échec de la requête :', error);
    throw error;
  }
}

Piège 4 : confondre asynchrone et parallèle

À l'intérieur d'une fonction async, le code reste séquentiel. Chaque await attend que la Promise précédente soit résolue. Si tu veux du parallélisme, tu dois lancer toutes les Promises avant d'attendre, ou utiliser Promise.all.

// Séquentiel : 600 ms total
async function chargerProfil() {
  const user = await fetch('/api/user');         // 200 ms
  const orders = await fetch('/api/orders');     // 200 ms
  const billing = await fetch('/api/billing');   // 200 ms
}

// Parallèle : 200 ms total
async function chargerProfil() {
  const [user, orders, billing] = await Promise.all([
    fetch('/api/user'),
    fetch('/api/orders'),
    fetch('/api/billing'),
  ]);
}

Piège 5 : oublier que async fonction renvoie toujours une Promise

Même si tu écris return 42 dans une fonction async, l'appelant recevra une Promise qui se résout à 42. Tu dois donc l'await ou enchaîner avec .then().

Piège 6 : les courses entre requêtes (race conditions)

Imagine un champ de recherche qui déclenche une requête à chaque frappe. L'utilisateur tape "ja" puis "jav" puis "java". Trois requêtes partent dans cet ordre. Mais rien ne garantit que les réponses arrivent dans l'ordre d'envoi. Tu peux très bien afficher les résultats de "ja" alors que l'utilisateur a déjà tapé "java".

// Solution avec AbortController
let controller;

async function rechercher(terme) {
  if (controller) controller.abort();
  controller = new AbortController();

  try {
    const response = await fetch(`/api/search?q=${terme}`, {
      signal: controller.signal,
    });
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') return;
    throw error;
  }
}

Chaque nouvelle frappe annule la requête précédente. Tu reçois uniquement la réponse de la dernière saisie. C'est un pattern qu'on retrouve dans toutes les barres de recherche bien faites.

Aller plus loin sans se perdre

Une fois que ces pièges sont intégrés, tu peux aborder les sujets connexes : annuler une requête avec AbortController, gérer un timeout, retry une requête qui échoue, ou structurer une couche d'accès aux données pour ne pas répéter le même try/catch dans cinquante composants.

À ce stade, tu commences à toucher à des questions d'architecture. Comment isoler les appels réseau du reste du code ? Comment tester une fonction qui dépend d'une API externe ? Ces questions débordent du cadre de l'asynchrone. L'article sur la Separation of Concerns donne un cadre pour ne pas tout mélanger dans le même fichier.

Si tu veux structurer ton apprentissage de bout en bout, la formation JavaScript avancé : async, POO et patterns modernes couvre l'event loop, les Promises, async/await, et les patterns d'architecture associés. Pour les fondations, la formation JavaScript fondamentaux reste la meilleure entrée si tu démarres.

Ce que tu dois retenir

  • JavaScript a un seul thread. L'asynchrone n'est pas du parallélisme, c'est une manière de ne pas figer le programme pendant qu'on attend.
  • Les callbacks marchent mais deviennent illisibles quand on les imbrique. Les Promises règlent ça avec le chaînage. async/await rend la syntaxe presque synchrone.
  • Une fonction async renvoie toujours une Promise, même si tu retournes une valeur primitive.
  • Un await dans une boucle séquentielle est un signal d'alarme. Pense à Promise.all dès qu'il s'agit d'opérations indépendantes.
  • fetch ne rejette pas la Promise sur un 404 ou un 500. Toujours vérifier response.ok avant de continuer.
  • try/catch entoure les await comme du code synchrone. C'est le seul vrai apport ergonomique de async/await.

L'asynchrone JavaScript est un de ces sujets où lire ne suffit pas. Tu dois écrire du code, te tromper, déboguer, recommencer. Mais une fois que le déclic se fait, tu ne reverras plus jamais un .then() avec les mêmes yeux.

Questions fréquentes

Faut-il encore apprendre les callbacks aujourd'hui ?

Oui, mais pas en premier. Tu rencontreras des callbacks dans des bibliothèques anciennes, dans Node.js (lecture de fichiers, événements), et dans certaines API web. Comprendre leur logique aide aussi à saisir pourquoi les Promises ont été inventées. Mais pour écrire du code neuf en 2026, async/await est la bonne valeur par défaut.

Quelle est la différence entre Promise.all et Promise.allSettled ?

Promise.all rejette dès qu'une seule des Promises échoue. Tu perds les résultats des autres, même si elles ont réussi. Promise.allSettled attend que toutes terminent, succès ou échec, et te renvoie un tableau d'objets décrivant l'état de chacune. Utilise allSettled quand un échec partiel est acceptable et que tu veux quand même les résultats des opérations qui ont marché.

Peut-on utiliser await en dehors d'une fonction async ?

Oui, depuis le top-level await dans les modules ES (extension .mjs ou type module dans package.json). Tu peux écrire await fetch(...) directement à la racine d'un fichier. En revanche, dans un script classique ou dans une fonction non-async, ça lèvera une erreur de syntaxe.

Pourquoi mon try/catch n'attrape pas certaines erreurs asynchrones ?

Un try/catch n'attrape que ce qui se passe dans son bloc, au moment où le code s'exécute. Si tu lances une opération asynchrone sans await, l'erreur arrive plus tard, en dehors du contexte du try. Le réflexe : await toutes les Promises dans le bloc try, ou attache un .catch() à celles que tu ne veux pas bloquer.

Async/await est-il plus lent que les Promises ?

Non. Async/await est compilé vers des Promises par les moteurs JavaScript modernes. La différence de performance est négligeable. Le seul piège, c'est de mal l'utiliser en mettant des await là où Promise.all ferait gagner du temps. Le ralentissement vient du code, pas du mot-clé.

Comment annuler une requête fetch en cours ?

Avec AbortController. Tu crées un controller, tu passes son signal à fetch, et tu appelles controller.abort() quand tu veux annuler. C'est utile dans React quand un composant se démonte avant la fin de la requête, ou quand l'utilisateur change de page. Sans ça, tu mets à jour un état d'un composant qui n'existe plus, et tu te retrouves avec des warnings ou des fuites mémoire.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous