Synchrone, asynchrone et boucle d'événements

Première leçon du cours JavaScript asynchrone (promesses, fetch, API), en accès libre : environ 15 minutes, 3 exercices et 3 questions de quiz. C'est la leçon de base du catalogue ; dans l'espace étudiant, elle est réécrite pour ton profil.

Objectifs

  • Distinguer code synchrone et code asynchrone
  • Expliquer le rôle de la pile d'appels, de la file des tâches et des microtâches
  • Prévoir l'ordre d'exécution d'un programme utilisant setTimeout et des callbacks

Boucle d'événementsFonctions de rappel (callbacks)setTimeout

Synchrone, asynchrone et boucle d’événements

JavaScript exécute votre code sur un seul fil d’exécution (single thread). Il ne fait qu’une chose à la fois : la fonction en cours se trouve en haut de la pile d’appels (call stack), et la suivante attend qu’elle se termine.

function saluer(nom) {
  return `Bonjour ${nom}`;
}
console.log("début");
console.log(saluer("Ada")); // saluer est empilée, exécutée puis dépilée
console.log("fin");
// Affiche : début, Bonjour Ada, fin (dans l'ordre)

Pourquoi l’asynchrone ?

Attendre une réponse réseau peut prendre une seconde. Si JavaScript bloquait le fil pendant ce temps, la page serait figée : plus de clics, plus d’animation. La solution : déléguer l’attente à l’environnement (navigateur ou Node.js) et donner une fonction à rappeler plus tard, un callback.

console.log("A");
setTimeout(() => {
  console.log("B"); // exécuté plus tard, par la boucle d'événements
}, 0);
console.log("C");
// Affiche : A, C, B

Même avec un délai de 0, B s’affiche en dernier. setTimeout ne lance pas la fonction : il demande à l’environnement de la placer dans la file des tâches une fois le délai écoulé.

La boucle d’événements

La boucle d’événements (event loop) répète en permanence ce cycle :

  1. exécuter le code présent dans la pile d’appels jusqu’à ce qu’elle soit vide ;
  2. exécuter toutes les microtâches en attente (suites de promesses, queueMicrotask) ;
  3. prendre une tâche dans la file (callback de setTimeout, événement de clic…) et recommencer.
setTimeout(() => console.log("tâche"), 0);
Promise.resolve().then(() => console.log("microtâche"));
console.log("synchrone");
// synchrone, microtâche, tâche

Le délai de setTimeout est donc un minimum, pas une garantie : si le fil est occupé par un calcul long, le callback attendra.

Le style callback

Avant les promesses, les API asynchrones prenaient un callback, souvent avec la convention Node.js « erreur en premier » :

function lireConfig(callback) {
  setTimeout(() => {
    const ok = true;
    if (!ok) return callback(new Error("Fichier introuvable"));
    callback(null, { theme: "sombre" }); // null = pas d'erreur
  }, 10);
}

lireConfig((err, config) => {
  if (err) return console.error(err.message);
  console.log(config.theme);
});

Ce style fonctionne, mais dès que plusieurs opérations dépendent les unes des autres, les callbacks s’imbriquent : c’est le callback hell. Chaque niveau doit gérer ses propres erreurs, et le code dérive vers la droite. Les promesses, vues dans la leçon suivante, résolvent ce problème en représentant un résultat futur par un objet que l’on peut enchaîner.

À retenir

  • Une seule chose à la fois dans la pile d’appels.
  • L’attente est déléguée ; la suite revient via la file des tâches ou des microtâches.
  • Les microtâches passent avant les tâches.

Exercices

Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.

  1. CodeDifficulté 1JavaScript dans le navigateur1 indice

    Prévoir l'ordre d'affichage

    La fonction ordre() doit renvoyer une promesse qui se résout avec le tableau des étiquettes dans l’ordre réel d’exécution. Complétez le code pour enregistrer chaque étiquette dans journal puis résoudre la promesse après le minuteur.

    Observez l’ordre obtenu : synchrone, microtâche, tâche.

    function ordre() {
      const journal = [];
      return new Promise((resolve) => {
        setTimeout(() => {
          journal.push("timeout");
          // TODO : résoudre la promesse avec journal
        }, 0);
        Promise.resolve().then(() => journal.push("promesse"));
        journal.push("synchrone");
      });
    }
  2. CodeDifficulté 2JavaScript dans le navigateur2 indices

    Convention « erreur en premier »

    Écrivez diviserPlusTard(a, b, callback) qui, après un setTimeout de 0 ms, appelle :

    • callback(new Error("Division par zéro")) si b === 0 ;
    • sinon callback(null, a / b).
    function diviserPlusTard(a, b, callback) {
      // TODO
    }
  3. QCMDifficulté 11 indice

    Microtâche ou tâche ?

    Que affiche ce code ?

    setTimeout(() => console.log('X'), 0);
    Promise.resolve().then(() => console.log('Y'));
    console.log('Z');
    • X, Y, Z
    • Z, Y, X
    • Z, X, Y
    • Y, Z, X

Quiz de fin de leçon

  1. Combien d’instructions JavaScript s’exécutent en même temps dans la pile d’appels d’un même fil ?

    • Une seule
    • Autant que de cœurs du processeur
    • Deux : une synchrone et une asynchrone
  2. Que garantit le délai passé à setTimeout ?

    • Une exécution exactement après ce délai
    • Un délai minimum avant que le callback puisse s'exécuter
    • Une exécution avant toutes les promesses
  3. Quel problème le « callback hell » désigne-t-il ?

    • Un callback qui n'est jamais appelé
    • L'imbrication profonde de callbacks qui rend le code illisible
    • Une erreur de syntaxe dans setTimeout

La suite du cours

  1. Les promesses : créer, enchaîner, gérer les erreurs18 min
  2. async/await et try/catch16 min
  3. Combiner les promesses : all, allSettled, race, any15 min
  4. fetch : consommer une API JSON20 min

JavaScript asynchrone (promesses, fetch, API) · Intermédiaire · 2 h 24