JavaScript exécute une seule chose à la fois. Quand une fonction en appelle une autre, il faut bien qu'il se souvienne d'où il vient pour y revenir une fois le travail terminé.
Ce carnet de retour porte un nom : la pile d'appels. La connaître change la façon de lire un message d'erreur, et explique pourquoi une récursivité mal bornée fait tomber le programme entier.
Définition
La pile d'appels, ou call stack, est la structure dans laquelle le moteur empile les fonctions en cours. Chaque appel ajoute une entrée au sommet, chaque return la retire. Le dernier entré est toujours le premier sorti.
function troisieme() {
throw new Error("quelque chose a cassé");
}
function deuxieme() { troisieme(); }
function premiere() { deuxieme(); }
try {
premiere();
} catch (erreur) {
console.log(erreur.stack.split("\n").slice(0, 4).join("\n"));
}
// Error: quelque chose a cassé
// at troisieme
// at deuxieme
// at premiereLa trace se lit de haut en bas comme un chemin inverse : la première ligne est l'endroit exact de l'incident, les suivantes disent qui a demandé quoi pour en arriver là.
Ce que la pile ne contient pas
C'est le point qui surprend le plus. Un setTimeout() ou une Promise ne restent pas sur la pile pendant leur attente : leur fonction de rappel est mise de côté et ne revient qu'une fois la pile entièrement vidée.
console.log("1 debut");
setTimeout(() => console.log("3 minuteur"), 0);
console.log("2 fin");
// 1 debut
// 2 fin
// 3 minuteurMême avec un délai de zéro, le minuteur passe en dernier. C'est le travail de l'Event loop (boucle d'événements), qui ne réalimente la pile que lorsqu'elle est vide. Conséquence directe : une fonction qui bloque la pile gèle tout le reste, y compris l'affichage.
Le débordement de pile
La pile a une taille limite. La dépasser produit une erreur immédiate, presque toujours causée par une Récursivité sans condition d'arrêt.
let profondeur = 0;
function descendre() {
profondeur += 1;
descendre();
}
try {
descendre();
} catch (erreur) {
console.log(erreur.constructor.name); // RangeError
console.log(profondeur > 1000); // true
}Un débordement n'accuse pas toujours une récursivité infinie. Un arbre très profond, ou deux fonctions qui s'appellent mutuellement, produisent la même erreur avec une logique pourtant correcte.
Questions fréquentes
Quelle est la taille maximale de la pile ?
Elle n'est fixée par aucune spécification et varie selon le moteur, le navigateur et la mémoire disponible. L'ordre de grandeur tourne autour de dix mille appels imbriqués. Un code qui s'en approche a de toute façon un problème de conception : la profondeur devrait dépendre des données, jamais du hasard.
Pourquoi ma trace d'erreur ne montre-t-elle que des noms illisibles ?
Parce que le code déployé est minifié : les noms de fonctions ont été raccourcis pour réduire le poids du fichier. La solution est de publier une Source map avec la version compilée, que l'outil de suivi utilise pour retrouver les noms et les numéros de ligne d'origine.
Une fonction asynchrone occupe-t-elle la pile pendant son attente ?
Non. Au moment de l'await, la fonction est retirée de la pile et son état mis de côté. Elle y revient quand le résultat arrive, ce qui libère entre-temps le moteur pour tout le reste. C'est exactement ce qui permet à une page de rester réactive pendant un appel réseau.