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.

Installer l'extension

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.