Jour 19 Day 19 · mercredi 26 août 2026 Wednesday 26 August 2026 Web Fondamental
JavaScript : event loop & async JavaScript: event loop & async
Call stack, microtasks, macrotasks, promises et async/await : le « dans quel ordre ça log ? » est LA question JavaScript la plus posée en entretien de stage front comme back. Call stack, microtasks, macrotasks, promises and async/await: the "in what order does it log?" question is THE most asked JavaScript question in front-end and back-end internship interviews alike.
L’essentiel
JavaScript exécute votre code sur un seul thread : une seule instruction à la fois, une seule call stack. Et pourtant un serveur Node encaisse des milliers de requêtes simultanées et une page web reste fluide pendant un fetch. Le secret n’est pas dans le langage mais dans son environnement d’exécution : l’event loop.
Le modèle : les opérations lentes (réseau, timers, disque) sont déléguées aux APIs de l’environnement (navigateur ou Node/libuv), qui travaillent en coulisses. Quand une opération se termine, son callback est mis en file d’attente ; l’event loop le dépile et l’exécute quand la call stack est vide. On ne bloque jamais en attendant : on s’inscrit pour être rappelé.
Il n’y a pas une file mais deux, et c’est là que se jouent tous les pièges d’entretien :
| Microtasks | Macrotasks (tasks) | |
|---|---|---|
| Sources | .then/.catch/.finally, await, queueMicrotask | setTimeout, setInterval, I/O, événements UI |
| Quand | Toutes, dès que la stack se vide | Une seule par tour de boucle |
| Priorité | Toujours avant la macrotask suivante | Après vidage complet des microtasks |
| Risque | Une chaîne infinie affame la boucle | Un callback lent gèle tout |
Comment ça marche
Un tour d’event loop : exécuter le code synchrone jusqu’à vider la call stack → vider entièrement la file des microtasks (y compris celles créées en cours de route) → prendre une macrotask → recommencer.
┌───────────────┐
│ Call stack │◀────────────────────┐
└───────┬───────┘ │
│ stack vide ? │
▼ │
┌───────────────────────┐ │
│ Microtasks (promises) │── vidées EN │
└───────────┬───────────┘ ENTIER d'abord │
▼ │
┌───────────────────────┐ │
│ Macrotasks (setTimeout,│── UNE seule, │
│ I/O, clics…) │ puis re-boucle┘
└───────────────────────┘
Web APIs / libuv travaillent en parallèle
et poussent les callbacks dans les files.
Le grand classique, à savoir dérouler ligne par ligne :
console.log("1"); // synchrone : direct
setTimeout(() => console.log("2"), 0); // macrotask, même à 0ms
Promise.resolve().then(() => console.log("3")); // microtask
(async () => {
console.log("4"); // synchrone ! avant le await
await null; // suspend : la suite = microtask
console.log("5");
})();
console.log("6");
// Ordre : 1, 4, 6, 3, 5, 2
// → tout le synchrone d'abord (1, 4, 6)
// → puis TOUTES les microtasks (3, puis 5)
// → puis la macrotask du setTimeout (2)
setTimeout(fn, 0) ne veut donc pas dire « immédiat » : ça veut dire « au plus tôt au prochain tour de boucle, après tout le synchrone et toutes les microtasks » — et si la stack est occupée 3 secondes, le callback attendra 3 secondes. Le délai est un minimum, pas une garantie.
🎤 En entretien — le « dans quel ordre ça log ? » est le grand classique absolu. La méthode qui marche : trois colonnes mentales (synchrone / microtasks / macrotasks), classer chaque ligne, puis lire colonne par colonne. Verbaliser ce classement à voix haute pendant l’exercice, c’est exactement ce que l’intervieweur veut entendre.
Concepts clés à maîtriser
- Promise : un objet représentant une valeur future, avec trois états —
pending, puis une seule foisfulfilledourejected(elle est alors settled, définitivement)..thenretourne une nouvelle promise, d’où le chaînage plat qui a remplacé le callback hell. - Combinateurs :
Promise.all(tout en parallèle, rejette dès le premier échec),Promise.allSettled(attend tout, donne les résultats ET les échecs),Promise.race(le premier settled gagne — pratique pour un timeout). async/await: du sucre syntaxique sur les promises. Une fonctionasyncretourne toujours une promise ;awaitsuspend la fonction (pas le thread !) et rend la main à l’event loop — la suite de la fonction devient une microtask.- Gestion d’erreurs :
try/catchautour d’await, ou.catch()sur la chaîne. Une rejection jamais capturée =unhandledRejection(crash possible en Node). Piège :try { maFonctionAsync() }sansawaitne capture rien — la promise rejette après la sortie dutry. - Le cas Node.js : même modèle (event loop libuv, phases timers → I/O → check), même règle d’or — ne jamais bloquer l’event loop. Un
JSON.parsede 50 Mo ou une boucle de calcul gèle toutes les requêtes du serveur, pas une seule. Pour le CPU-bound :worker_threads(vrais threads avec leur propre event loop) ou découper le travail.
💡 Parallèle vs séquentiel —
await a(); await b();exécute en séquence (2× le temps).await Promise.all([a(), b()])lance les deux en même temps. Repérer desawaiten série sur des opérations indépendantes est une des remarques les plus faciles à placer en revue de code… et en entretien.
En entretien
« JavaScript est single-threaded : comment gère-t-il 1000 requêtes simultanées ? » — Le thread JS n’exécute que du code court ; les opérations lentes (réseau, disque) sont déléguées à l’environnement (Web APIs, libuv) qui, lui, est asynchrone/multithreadé. Les callbacks reviennent par les files, dépilées quand la stack est vide. La concurrence vient de l’attente entrelacée, pas du parallélisme du code JS.
« Pourquoi setTimeout(fn, 0) n’exécute-t-il pas fn immédiatement ? » — Parce que fn devient une macrotask : elle attend la fin du code synchrone en cours ET le vidage complet des microtasks. 0 ms = délai minimum avant mise en file, jamais un rendez-vous.
« Microtask vs macrotask ? » — Deux files distinctes. Microtasks (callbacks de promises, await) : la file est vidée entièrement dès que la stack se libère, avant toute macrotask. Macrotasks (setTimeout, I/O, événements) : une seule par tour de boucle. Conséquence directe : un .then passe toujours avant un setTimeout(0) armé au même moment.
« Différence entre Promise.all et Promise.allSettled ? » — all rejette dès le premier échec (fail-fast) : bien quand tout est indispensable. allSettled attend toutes les promises et retourne {status, value|reason} pour chacune : bien pour des opérations indépendantes dont on veut le bilan complet.
« C’est quoi “bloquer l’event loop” en Node, et que faire pour du CPU-bound ? » — Toute tâche synchrone longue (gros parse, crypto, boucle de calcul) monopolise l’unique thread : plus aucune requête n’est servie pendant ce temps. Solutions : worker_threads pour déporter le calcul, découper en morceaux (setImmediate), ou déléguer à un service dédié.
Pièges & idées reçues
⚠️
awaitdans une boucle —for (const u of urls) { await fetch(u); }télécharge un par un. Si les requêtes sont indépendantes :await Promise.all(urls.map(u => fetch(u))). C’est le piège de performance async le plus courant en projet étudiant — et les intervieweurs le savent.
- « async/await rend le code multithread » — non : c’est toujours un seul thread.
awaitsuspend la fonction, jamais le thread ; entre-temps, l’event loop exécute autre chose. forEach+asyncne s’attend pas :array.forEach(async x => …)n’attend rien du tout (forEachignore les promises retournées). Utiliserfor…of+await, ouPromise.all(array.map(…)).- Une promise démarre à sa création, pas au
.then:const p = fetch(url)lance déjà la requête. Le.thenne fait que s’abonner au résultat. - Oublier qu’
awaitrend la main : entreawaitet la ligne suivante, l’état du programme a pu changer (autre requête traitée, variable partagée modifiée). Source de bugs de concurrence subtils, même en single-thread. process.nextTick(Node) passe même avant les microtasks promises — à connaître de nom, à éviter en pratique.
Pour aller plus loin
- MDN — The event loop et Using promises
- Jake Archibald — In the Loop (JSConf) : LA conférence de référence, visuelle et mémorable
- Node.js — Don’t block the event loop : le guide officiel côté serveur
- S’entraîner : écrire des snippets mêlant
setTimeout,.thenetasync/await, prédire l’ordre sur papier, vérifier dans la console — dix minutes par jour jusqu’à ne plus se tromper
The essentials
JavaScript runs your code on a single thread: one instruction at a time, one call stack. Yet a Node server handles thousands of simultaneous requests and a web page stays smooth during a fetch. The secret is not in the language but in its runtime environment: the event loop.
The model: slow operations (network, timers, disk) are delegated to the environment’s APIs (browser or Node/libuv), which work behind the scenes. When an operation completes, its callback is queued; the event loop dequeues and runs it when the call stack is empty. You never block while waiting: you register to be called back.
There isn’t one queue but two, and that’s where every interview trap lives:
| Microtasks | Macrotasks (tasks) | |
|---|---|---|
| Sources | .then/.catch/.finally, await, queueMicrotask | setTimeout, setInterval, I/O, UI events |
| When | All of them, as soon as the stack empties | One per loop iteration |
| Priority | Always before the next macrotask | After the microtask queue is fully drained |
| Risk | An endless chain starves the loop | A slow callback freezes everything |
How it works
One event loop turn: run synchronous code until the call stack is empty → drain the microtask queue entirely (including microtasks created along the way) → take one macrotask → repeat.
┌───────────────┐
│ Call stack │◀────────────────────┐
└───────┬───────┘ │
│ stack empty? │
▼ │
┌───────────────────────┐ │
│ Microtasks (promises) │── drained IN │
└───────────┬───────────┘ FULL first │
▼ │
┌───────────────────────┐ │
│ Macrotasks (setTimeout,│── ONE only, │
│ I/O, clicks…) │ then loop back┘
└───────────────────────┘
Web APIs / libuv work in parallel and
push callbacks into the queues.
The great classic, to be able to walk through line by line:
console.log("1"); // synchronous: runs now
setTimeout(() => console.log("2"), 0); // macrotask, even at 0ms
Promise.resolve().then(() => console.log("3")); // microtask
(async () => {
console.log("4"); // synchronous! before the await
await null; // suspends: the rest = microtask
console.log("5");
})();
console.log("6");
// Order: 1, 4, 6, 3, 5, 2
// → all synchronous code first (1, 4, 6)
// → then ALL the microtasks (3, then 5)
// → then the setTimeout macrotask (2)
So setTimeout(fn, 0) does not mean “immediately”: it means “at the earliest on the next loop turn, after all synchronous code and all microtasks” — and if the stack is busy for 3 seconds, the callback waits 3 seconds. The delay is a minimum, not a guarantee.
🎤 In an interview — the “in what order does it log?” exercise is the absolute classic. The method that works: three mental columns (synchronous / microtasks / macrotasks), sort each line into one, then read column by column. Verbalizing that sorting out loud during the exercise is exactly what the interviewer wants to hear.
Key concepts to master
- Promise: an object representing a future value, with three states —
pending, then exactly oncefulfilledorrejected(it is then settled, permanently)..thenreturns a new promise, hence the flat chaining that replaced callback hell. - Combinators:
Promise.all(everything in parallel, rejects on the first failure),Promise.allSettled(waits for everything, returns results AND failures),Promise.race(first settled wins — handy for a timeout). async/await: syntactic sugar over promises. Anasyncfunction always returns a promise;awaitsuspends the function (not the thread!) and yields to the event loop — the rest of the function becomes a microtask.- Error handling:
try/catcharoundawait, or.catch()on the chain. A never-caught rejection =unhandledRejection(possible crash in Node). Trap:try { myAsyncFn() }withoutawaitcatches nothing — the promise rejects after thetryhas exited. - The Node.js case: same model (libuv event loop, phases timers → I/O → check), same golden rule — never block the event loop. A 50 MB
JSON.parseor a compute loop freezes all the server’s requests, not just one. For CPU-bound work:worker_threads(real threads with their own event loop) or split the work into chunks.
💡 Parallel vs sequential —
await a(); await b();runs in sequence (2× the time).await Promise.all([a(), b()])starts both at once. Spotting serialawaits on independent operations is one of the easiest remarks to make in a code review… and in an interview.
In an interview
“JavaScript is single-threaded: how does it handle 1000 simultaneous requests?” — The JS thread only runs short code; slow operations (network, disk) are delegated to the environment (Web APIs, libuv), which is asynchronous/multithreaded. Callbacks come back through the queues, dequeued when the stack is empty. Concurrency comes from interleaved waiting, not from parallel JS code.
“Why doesn’t setTimeout(fn, 0) run fn immediately?” — Because fn becomes a macrotask: it waits for the current synchronous code to finish AND the microtask queue to fully drain. 0 ms = minimum delay before queueing, never an appointment.
“Microtask vs macrotask?” — Two distinct queues. Microtasks (promise callbacks, await): the queue is fully drained as soon as the stack frees up, before any macrotask. Macrotasks (setTimeout, I/O, events): one per loop turn. Direct consequence: a .then always runs before a setTimeout(0) armed at the same moment.
“Difference between Promise.all and Promise.allSettled?” — all rejects on the first failure (fail-fast): right when everything is required. allSettled waits for all promises and returns {status, value|reason} for each: right for independent operations where you want the full report.
“What does ‘blocking the event loop’ mean in Node, and what about CPU-bound work?” — Any long synchronous task (big parse, crypto, compute loop) monopolizes the single thread: no request is served in the meantime. Solutions: worker_threads to offload the computation, splitting into chunks (setImmediate), or delegating to a dedicated service.
Pitfalls & misconceptions
⚠️
awaitin a loop —for (const u of urls) { await fetch(u); }downloads one at a time. If the requests are independent:await Promise.all(urls.map(u => fetch(u))). It’s the most common async performance trap in student projects — and interviewers know it.
- “async/await makes the code multithreaded” — no: still one thread.
awaitsuspends the function, never the thread; meanwhile, the event loop runs something else. forEach+asyncdoesn’t wait:array.forEach(async x => …)waits for nothing at all (forEachignores returned promises). Usefor…of+await, orPromise.all(array.map(…)).- A promise starts at creation, not at
.then:const p = fetch(url)already fires the request..thenmerely subscribes to the result. - Forgetting that
awaityields: betweenawaitand the next line, program state may have changed (another request handled, a shared variable modified). A source of subtle concurrency bugs, even single-threaded. process.nextTick(Node) runs even before promise microtasks — know the name, avoid it in practice.
Going further
- MDN — The event loop and Using promises
- Jake Archibald — In the Loop (JSConf): THE reference talk, visual and memorable
- Node.js — Don’t block the event loop: the official server-side guide
- Practice: write snippets mixing
setTimeout,.thenandasync/await, predict the order on paper, check in the console — ten minutes a day until you never get it wrong