JavaScript s'exécute sur un seul thread : à un instant donné, une seule instruction tourne. Pourtant, une application web peut gérer des dizaines de requêtes réseau, timers et interactions utilisateur sans jamais geler l'interface. Ce tour de passe-passe, c'est l'Event Loop.
Le call stack, la pile d'exécution
Chaque fonction appelée est empilée sur le call stack, et dépilée quand elle retourne. Tant que le stack n'est pas vide, le thread est occupé : c'est pour ça qu'une boucle infinie synchrone bloque tout, y compris le rendu de la page.
Les Web APIs et la file de tâches
Quand on appelle setTimeout, fetch ou qu'on écoute un événement DOM, le travail n'est pas fait par le moteur JS lui-même mais délégué au navigateur (ou à Node.js). Une fois terminé, le callback correspondant est déposé dans une file d'attente plutôt qu'exécuté immédiatement.
Macrotasks vs microtasks
Il existe en réalité deux files distinctes. Les macrotasks (setTimeout, setInterval, les événements I/O) et les microtasks (les .then() de Promise, queueMicrotask, async/await). L'Event Loop applique une règle simple : après chaque macrotask, il vide entièrement la file de microtasks avant de passer à la suivante.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Ordre affiché : 1, 4, 3, 2
// Le code synchrone passe d'abord, puis les microtasks (Promise),
// puis enfin les macrotasks (setTimeout) — même avec un délai de 0ms.Comprendre cet ordre évite des bugs classiques : un setTimeout(fn, 0) n'est jamais vraiment "immédiat", et une Promise résolue passera toujours avant lui. C'est cette mécanique qui rend async/await si prévisible malgré son apparence synchrone.
