Deux callbacks inscrits l'un après l'autre ne passent pas forcément dans cet ordre. Celui d'une promesse devance celui d'un minuteur, même écrit après lui.
La raison tient à une file distincte, servie en priorité par la boucle d'événements, et dont l'existence explique la plupart des ordres d'exécution surprenants.
Définition
Une microtâche est un travail court placé dans une file servie intégralement dès que la pile d'appels se vide, avant que la moindre tâche ordinaire ne soit reprise. Trois choses en produisent.
- Un then(), un catch ou un finally sur une promesse déjà réglée ou qui vient de l'être.
- La reprise après un await, qui est exactement la même mécanique écrite autrement.
- Un appel à
queueMicrotask, la façon explicite d'en inscrire une.
setTimeout(() => console.log("tâche"), 0);
queueMicrotask(() => console.log("microtâche"));
Promise.resolve().then(() => console.log("promesse"));
console.log("synchrone");
// synchrone
// microtâche
// promesse
// tâcheMicrotâche et tâche : la différence qui compte
| Critère | Microtâche | Tâche |
|---|---|---|
| Origine | Promesses, queueMicrotask | Minuteurs, événements, requêtes |
| Nombre par tour | Toute la file, sans interruption | Une seule, puis la boucle repasse |
| Rendu de la page | Aucun avant la fin de la file | Possible entre deux tâches |
Ces trois lignes expliquent le comportement observé : la file des microtâches est vidée d'un bloc, et une microtâche qui en inscrit une autre allonge le même tour au lieu d'attendre le suivant.
Affamer la boucle
Cette priorité a un prix. Une microtâche qui s'inscrit elle-même sans condition d'arrêt occupe la file indéfiniment : les minuteurs ne se déclenchent plus, les clics ne sont plus traités, et la page se fige sans qu'aucune boucle infinie n'apparaisse dans le code.
// À ne pas exécuter : la boucle d'événements ne reprend jamais la main
// function boucler() { queueMicrotask(boucler); }
// boucler();
// La version correcte, qui laisse respirer la boucle
function bouclerProprement(reste) {
if (reste === 0) return console.log("terminé");
setTimeout(() => bouclerProprement(reste - 1), 0);
}
bouclerProprement(3);Une récursion écrite avec await a le même défaut si rien ne rend la main : chaque reprise est une microtâche. Insérer une attente réelle, même d'une milliseconde, suffit à repasser par la file des tâches et à débloquer l'interface.
Questions fréquentes
Pourquoi préférer queueMicrotask à setTimeout(fn, 0) ?
Parce que la microtâche s'exécute avant tout rendu et avant toute autre tâche, ce qui garantit qu'aucun état intermédiaire n'est visible. setTimeout laisse au contraire la page se rafraîchir entre-temps, ce qui est parfois exactement ce que l'on veut.
L'ordre entre deux microtâches est-il garanti ?
Oui, la file est strictement premier entré, premier sorti. Deux then posés sur des promesses déjà tenues s'exécutent dans l'ordre d'inscription. C'est cette garantie qui rend le comportement des promesses reproductible d'une exécution à l'autre.
Faut-il connaître ce détail pour écrire du code correct ?
Pas au quotidien, mais il devient indispensable dès qu'un ordre inattendu apparaît, notamment dans les tests : une assertion écrite juste après un appel asynchrone s'exécute avant la reprise de celui-ci. Comprendre la file explique alors l'écart en quelques secondes.