Un diagnostic plus rapide des erreurs JavaScript grâce à Error.stack

IE10, disponible dans Windows 8 Consumer Preview intègre la prise en charge d'Error.stack, qui permet aux développeurs Web de diagnostiquer et de corriger les bogues plus rapidement, en particulier les bogues les plus difficiles à reproduire. Grâce aux plateformes Web des navigateurs modernes, les développeurs peuvent mettre au point des applications fantastiques. Dans Windows 8, cette puissance s'exprime à travers Internet Explorer 10 et les applications de style Metro développées en JavaScript. En raison de la puissance et de la complexité croissantes de ces applications, les développeurs ont besoin d'outils toujours plus performants tels que Error.stack pour traiter les erreurs et diagnostiquer les bogues.

Débogage des applications

En JavaScript, la gestion structurée des erreurs s'appuie sur les fonctions throw et try/catch : le développeur déclare une erreur et transmet le flux de contrôle à la partie du programme chargée de la gestion des erreurs. Lorsqu'une erreur est détectée, Chakra, le moteur JavaScript d'Internet Explorer, collecte la chaîne d'appels à l'origine de l'erreur, également appelée « pile des appels ». Si l'objet généré est un objet Error (ou s'il s'agit d'une fonction dont la chaîne prototype renvoie à Error), Chakra crée une arborescence d'appels de procédure, c'est-à-dire une liste lisible à l'œil de la pile des appels. Cette liste est représentée sous forme de propriété, stack, dans l'objet Error. La propriété stack inclut différentes informations relatives aux fonctions : message d'erreur, nom et emplacement du fichier source. Forts de ces informations, les développeurs peuvent diagnostiquer rapidement les défauts en identifiant la fonction appelée et même connaître la ligne de code à l'origine du problème. Ainsi, ces informations peuvent par exemple indiquer que la valeur d'un paramètre transmis à la fonction était null ou que son type n'était pas valide.

Étudions un exemple de script simple qui vise à calculer la distance entre deux points, (0, 2) et (12, 10) :

(function () {

'use strict';

function squareRoot(n) {

if (n < 0)

throw new Error('Cannot take square root of negative number.');

 

return Math.sqrt(n);

}

 

function square(n) {

return n * n;

}

 

function pointDistance(pt1, pt2) {

return squareRoot((pt1.x - pt2.x) + (pt1.y - pt2.y));

}

 

function sample() {

var pt1 = { x: 0, y: 2 };

var pt2 = { x: 12, y: 10 };

 

console.log('Distance is: ' + pointDistance(pt1, pt2));

}

 

try {

sample();

}

catch (e) {

console.log(e.stack);

}

})();

Ce script comporte un bogue : il oublie d'élever au carré les différences des composants. Par conséquent, dans certains cas, la fonction pointDistance renverra simplement des résultats incorrects. Dans d'autres cas, elle générera une erreur. Pour comprendre l'arborescence des appels de procédure, examinons l'erreur dans les Outils de développement F12 et étudions le contenu de l'onglet Script :

Capture d'écran des Outils de développement F12, montrant une arborescence d'appels de procédure enregistrée en appelant console.log(e.stack), où e correspond à l'objet Error transmis à la clause Catch d'un bloc Try/Catch.

L'arborescence d'appels de procédure est exportée vers la console dans la clause catch. Comme elle se trouve en haut de la pile, il est tout de suite évident que l'origine de l'erreur se trouve dans la fonction squareRoot. Pour procéder au débogage, le développeur n'a pas besoin de descendre très loin dans l'arborescence des appels de procédure : la condition préalable à la fonction squareRoot a été enfreinte. Il suffit d'observer le niveau supérieur pour comprendre pourquoi : les sous-expressions figurant dans l'appel de la fonction squareRoot doivent elles-mêmes être des paramètres de square.

Lors du débogage, la propriété stack facilite l'identification du code, pour définir un point d'arrêt. N'oubliez pas qu'il existe d'autres moyens d'afficher la pile des appels : par exemple, si vous activez le mode « Break on caught exception » du débogueur de scripts, vous pourrez peut-être inspecter la pile des appels à l'aide du débogueur. Dans le cas d'applications déployées, vous pouvez envisager d'inclure le code qui pose problème dans un wrapper au sein d'une fonction try/catch, afin de collecter les appels qui échouent et de les consigner sur votre serveur. Les développeurs peuvent ainsi examiner la pile des appels pour isoler plus facilement les zones présentant des problèmes.

Exceptions DOM et Error.stack

J'ai précédemment signalé que l'objet généré doit être un objet Error ou mener à un objet Error, par le biais de sa chaîne prototype. Ce principe est intentionnel : JavaScript prend en charge la génération d'un objet et même des primitives sous forme d'exceptions. Si tous ces objets peuvent être interceptés et examinés, ils ne sont pas tous spécialement conçus pour intégrer des erreurs ou des informations de diagnostic. Par conséquent, seuls les objets Error seront mis à jour par le biais d'une propriété stack lors de la génération.

Même si les exceptions DOM sont des objets, elles ne possèdent pas de chaînes prototypes qui mènent à Error. Par conséquent, elles ne possèdent pas non plus de propriété stack. Dans les scénarios où vous manipulez le DOM et où vous souhaitez exposer des erreurs compatibles avec JavaScript, il peut être judicieux d'inclure votre code de manipulation du DOM dans un wrapper au sein d'un bloc try/catch , puis de générer un nouvel objet Error dans la clause catch :

function causesDomError() {

try {

var div = document.createElement('div');

div.appendChild(div);

} catch (e) {

throw new Error(e.toString());

}

}

Cependant, il est judicieux d'étudier si ce modèle peut ou non vous être utile. Il est en général plus adapté au développement d'une bibliothèque d'utilitaires. Réfléchissez notamment à l'intention de votre code : vise-t-il à masquer la manipulation du DOM ou plus simplement à mener à bien une tâche spécifique ? Si l'objectif est de masquer la manipulation du DOM, la meilleure solution consiste sans doute à inclure la manipulation dans un wrapper et à générer un objet Error.

Remarques relatives aux performances

La construction de l'arborescence des appels de procédure commence dès la génération de l'objet Error. Pour cela, il convient de suivre la pile d'exécution en cours. Pour éviter les problèmes de performances pendant l'examen d'une pile particulièrement grande (ou même d'une chaîne de piles récursive), par défaut IE collecte uniquement les 10 frames de pile principales. Ce paramètre peut néanmoins être configuré en attribuant une autre valeur à la propriété statique Error.stackTraceLimit. Ce paramètre est global et doit être modifié avant de générer l'erreur. Dans le cas contraire, il sera sans effet sur les arborescences des appels de procédure.

Exceptions asynchrones

Lorsqu'une arborescence d'appels de procédure est générée à partir d'un rappel asynchrone (timeout, interval ou XMLHttpRequest, par exemple), c'est le rappel asynchrone et non pas le code qui a créé le rappel asynchrone, qui se trouve au bas de la pile des appels. Ceci peut potentiellement avoir des implications dans le cadre de l'identification du code défaillant : si vous utilisez la même fonction de rappel pour plusieurs rappels asynchrones, il peut être difficile de déterminer le rappel à l'origine de l'erreur à l'aide d'une simple inspection. Modifions légèrement notre exemple précédent, de sorte qu'au lieu d'appeler directement sample(), nous placions cet appel dans un rappel timeout :

(function () {

'use strict';

function squareRoot(n) {

if (n < 0)

throw new Error('Cannot take square root of negative number.');

 

return Math.sqrt(n);

}

 

function square(n) {

return n * n;

}

 

function pointDistance(pt1, pt2) {

return squareRoot((pt1.x - pt2.x) + (pt1.y - pt2.y));

}

 

function sample() {

var pt1 = { x: 0, y: 2 };

var pt2 = { x: 12, y: 10 };

 

console.log('Distance is: ' + pointDistance(pt1, pt2));

}

 

setTimeout(function () {

try {

sample();

}

catch (e) {

console.log(e.stack);

}

}, 2500);

})();

Lors de l'exécution de ce fragment de code, vous constaterez que l'arborescence des appels de procédure apparaît au terme d'un petit délai. Cette fois, vous constaterez également que le bas de la pile n'est pas Global Code, mais Anonymous function. En réalité, il ne s'agit pas de la même fonction anonyme, mais de la fonction de rappel transmise dans setTimeout. Puisque vous perdez le contexte entourant la liaison du rappel, vous ne pourrez peut-être pas déterminer les causes du rappel. Si vous envisagez un scénario dans lequel un rappel est enregistré pour gérer l'événement click de nombreux boutons, vous ne pourrez pas identifier à quel rappel correspond l'enregistrement. Cela dit, cette limitation est mineure, car dans la plupart des cas, le haut de la pile mettra en évidence les zones qui posent problème.

Exploration de la démonstration Test Drive

Capture d'écran de la démonstration Test Drive - Exploration d'Error.stack

Découvrez cette démonstration Test Drive en utilisant IE10 dans Windows 8 Consumer Preview. Vous pouvez exécuter du code dans le contexte d'une fonction eval. En cas d'erreur, vous pouvez l'inspecter. Si vous exécutez le code dans IE10, vous pouvez également mettre en évidence les lignes de votre code lorsque vous survolez les lignes d'erreur dans l'arborescence des appels de procédure. Vous pouvez saisir du code vous-même dans la zone Code ou faire votre choix parmi les exemples figurant dans la liste. Vous pouvez également configurer la valeur Error.stackTraceLimit lors de l'exécution des exemples de code.

Pour obtenir des informations de référence, consultez la documentation MSDN consacrée à Error.stack et stackTraceLimit.

—Rob Paveza, chef de projet, runtime Chakra