
Une trace de pile est la liste des appels de fonction en cours au moment où quelque chose a levé une exception. Le navigateur l’écrit de l’intérieur vers l’extérieur : la fonction qui a échoué en haut, celle qui l’a appelée juste en dessous, et ainsi de suite jusqu’à ce qui a déclenché le tout.
Elle ressemble à un mur de texte et on la traite comme tel. La plupart des gens lisent la première ligne, recherchent le message et ne regardent jamais le reste, ce qui est dommage, car la première ligne dit ce qui a cassé et les lignes du dessous disent pourquoi on en est arrivé là.
Ce que sont vraiment ces lignes
Une trace de navigateur ressemble en gros à ceci :
TypeError: Cannot read properties of undefined (reading 'total')
at formatPrice (checkout.js:214:19)
at renderSummary (checkout.js:188:7)
at onCartLoaded (cart.js:97:5)
at HTMLButtonElement.<anonymous> (cart.js:41:12)
Quatre éléments s’y trouvent, et chacun répond à une question différente.
La première ligne est l’erreur : un type et un message. Elle dit ce qui s’est mal passé au
moment où cela s’est produit, et c’est la ligne la moins utile pour remonter à la cause. Dans
l’exemple, une partie de formatPrice a tenté de lire .total sur une valeur absente, ce qui
correspond au
cas de la propriété undefined
et n’est presque jamais la faute de formatPrice.
Chaque ligne at est une frame : une fonction qui attendait que celle du dessus lui rende la
main. formatPrice a été appelée par renderSummary, elle-même appelée par onCartLoaded.
Les nombres indiquent le fichier, la ligne et la colonne. checkout.js:214:19 correspond à
la ligne 214, colonne 19, ce qui compte quand plusieurs appels se trouvent sur une même ligne.
La frame du bas est le déclencheur, et c’est généralement la ligne la plus instructive de
toute la trace. HTMLButtonElement.<anonymous> signifie qu’un gestionnaire de clic est à
l’origine de tout cela. Quelque chose fait par une personne, plutôt que par la page au
chargement. C’est déjà une étape de reproduction.
Lisez-la de bas en haut
L’habitude à prendre consiste à lire la trace dans l’ordre où le code s’est réellement exécuté, c’est-à-dire du bas vers le haut.
Un clic a été déclenché. onCartLoaded s’est exécutée. Elle a appelé renderSummary, qui a
appelé formatPrice, qui n’a rien trouvé là où elle attendait un total de panier. Lue ainsi, la
question n’est plus « qu’est-ce qui cloche dans formatPrice » mais « qu’avait onCartLoaded
entre les mains, et d’où cela venait-il ? ». C’est là que se trouve presque toujours le
correctif.
- Faible
- Erreur dans formatPrice, ligne 214. Ajout d'une vérification pour undefined.
- Meilleur
- Le gestionnaire de clic dans cart.js:41 s'est exécuté avant que la réponse du panier n'arrive, si bien que renderSummary a reçu un objet vide. L'ordre a été corrigé ; formatPrice reste inchangée.
La première version fait disparaître l’erreur. La seconde fait disparaître le bug. Ce n’est pas le même travail, et c’est la trace qui permet de faire la différence.
Les frames qui ne sont pas les vôtres
La plupart des traces réelles sont plus longues que l’exemple et pleines de frames provenant de bibliothèques : React, jQuery, le runtime d’un bundler, un polyfill. La plupart du temps, ce n’est que du bruit, et les deux principaux navigateurs peuvent les masquer pour vous.
Dans Chrome DevTools, la fonctionnalité s’appelle Ignore list, sous Settings ; elle replie
les frames des scripts que vous désignez et empêche le débogueur d’entrer dedans en pas à pas.
Elle ignore déjà /node_modules/ et /bower_components/ par défaut, donc la première chose
utile à faire est souvent d’y ajouter vos propres chemins de dépendances tierces. Le débogueur
de Firefox propose la même chose, fichier par fichier, sous le nom Ignore source. Dans les
deux cas, chaque trace suivante fait moitié moins de lignes.
Ce qui reste, c’est votre propre code, la partie que vous pouvez changer.
Un cas particulier à surveiller : si les seules frames restantes appartiennent à des bibliothèques, l’appel vient probablement de l’intérieur de cette bibliothèque plutôt que de votre code, et la cause est généralement les données ou les options que vous lui avez passées.
Pourquoi la trace pointe parfois vers n’importe quoi
Trois choses rendent régulièrement une trace illisible, et toutes trois tiennent à la façon dont le code a été livré, pas au bug lui-même.
La minification. En production, votre code tient sur une seule ligne gigantesque avec des
noms d’une seule lettre, si bien que la trace affiche at n (app.min.js:1:84213). Le numéro de
ligne est réel mais inutile. La solution, ce sont les source maps : un build génère un fichier
.map, le navigateur l’applique, et DevTools affiche les noms de fichiers et les lignes
d’origine. Vérifiez si les vôtres sont bien générées et si le navigateur peut y accéder, avant
de conclure qu’une trace est irrécupérable.
Les frontières asynchrones. Une trace s’arrête là où s’arrête la pile d’appels, et un
setTimeout, un callback de promesse ou un event listener en démarre une nouvelle. La frame qui
a planifié le travail n’apparaît pas dans la trace par défaut. Les Async stack traces de
Chrome sont activées dans DevTools et recousent les deux ensemble, ce qui explique pourquoi une
trace paraît plus complète dans DevTools que la même erreur dans un fichier de log.
Les scripts cross-origin. Si un script est servi depuis un autre domaine sans autorisation
d’exposer ses erreurs, le navigateur refuse purement et simplement de dire quoi que ce soit à ce
sujet : le message se réduit à la chaîne brute Script error., sans fichier, sans ligne et sans
trace. Le correctif se fait côté serveur : Access-Control-Allow-Origin sur le script, plus
crossorigin="anonymous" sur la balise. Bon à savoir, car le symptôme ressemble à une erreur
mystérieuse alors qu’il s’agit en réalité d’une règle de permissions.
La trace que vous n’avez pas collectée
Tout ce qui précède suppose que vous avez la trace. Le cas gênant est aussi celui qui compte le plus : l’erreur s’est produite sur la machine de quelqu’un d’autre, dans un navigateur que vous n’avez pas, et ce qui vous est parvenu, c’est une phrase.
Une trace de pile n’est pas quelque chose qu’on peut demander à un utilisateur. Elle vit dans un panneau qu’il n’a jamais ouvert, elle disparaît dès qu’il ferme l’onglet, et la console ne survit pas à un changement de page à moins que quelqu’un ait pensé à cocher Preserve log au préalable. Le choix pratique se résume donc à disposer d’un error tracker ou d’un outil de signalement qui capture la console au moment de l’échec, ou à s’en passer.
Sans cela, le rapport dit « la page de paiement est devenue blanche », et les quatre lignes qui auraient répondu à la question en une minute ont été jetées par le navigateur quelques secondes plus tard.
Session Replay
Extension Chrome gratuite. Un clic sur la page qui pose problème enregistre la capture d'écran, la console et le journal réseau, et vous fournit un lien à coller dans le ticket.
Provoquer une trace volontairement
Deux choses valent la peine d’être connues pour le jour où vous voulez une trace plutôt que d’attendre qu’elle survienne.
console.trace() affiche la pile d’appels actuelle sans lever d’exception, ce qui est le moyen
le plus rapide de répondre à « qui appelle cette fonction ? » dans une base de code que vous ne
connaissez pas.
new Error().stack vous donne la même chose sous forme de chaîne, ce que les error trackers
remontent à leur serveur. C’est aussi comme cela qu’on attache une trace à une erreur
interceptée qui, sinon, disparaîtrait : intercepter une exception et ne logger que e.message
jette toute la pile à la poubelle, et cette habitude est responsable d’une bonne partie des logs
de production illisibles.
Ce qu’il faut en retenir
La ligne du haut nomme le symptôme. La ligne du bas nomme ce qui l’a déclenché. Le milieu est le chemin entre les deux, et le correctif se trouve généralement quelque part sur ce chemin plutôt qu’à l’une ou l’autre extrémité.
Lisez-la de bas en haut, masquez les frames qui ne vous appartiennent pas, et vérifiez vos source maps avant d’accuser la trace. Et si la trace dont vous avez besoin appartient à la session de navigateur de quelqu’un d’autre, le moment pour vous organiser, c’est avant le bug, pas après : le temps que le rapport arrive, elle a déjà disparu.