Chapitre 1218 minDébutant
Déboguer JavaScript avec les DevTools et gérer les erreurs
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- lire un message d'erreur et retrouver la ligne en cause ;
- utiliser la console au-delà de
console.log; - poser un point d'arrêt et exécuter le code pas à pas ;
- analyser une requête dans l'onglet Réseau ;
- lever et intercepter des erreurs avec
throwettry…catch.
4 exercices corrigés
Déboguer, c'est trouver pourquoi un programme ne fait pas ce qu'on attend, puis le corriger ; dans le navigateur, on s'appuie pour cela sur les outils de développement (DevTools) : la console, le débogueur et l'onglet Réseau. Ce chapitre montre comment lire un message d'erreur, poser un point d'arrêt et avancer pas à pas, analyser une requête, puis comment ton propre code doit lever et intercepter les erreurs avec throw et try…catch. La démarche générale (trace d'exécution, jeux d'essai) est expliquée dans le chapitre Trace d'exécution et débogage du cours d'algorithmique.
Ouvrir les outils de développement
F12, ou Ctrl + Maj + I (Windows, Linux), Cmd + Option + I (macOS). Les onglets portent des noms un peu différents selon le navigateur :
| Usage | Chrome et Edge | Firefox | Safari |
|---|---|---|---|
| Voir et modifier le DOM et le CSS | Éléments | Inspecteur | Éléments |
| Lire les messages, tester une ligne | Console | Console | Console |
| Points d'arrêt, pas à pas | Sources | Débogueur | Sources |
| Requêtes HTTP | Réseau | Réseau | Réseau |
| localStorage, cookies, cache | Application | Stockage | Stockage |
| Accessibilité, performances | Lighthouse, Performances | Accessibilité, Performances | Audit, Chronologies |
Les exemples de ce chapitre suivent Chrome et Edge, qui partagent les mêmes outils ; les fonctions existent sous un autre nom dans Firefox et Safari.
La console, au-delà de console.log
| Méthode | Usage |
|---|---|
console.log(a, b) | Afficher des valeurs ; avec plusieurs arguments, elles s'affichent côte à côte |
console.log({ quantite, seuil }) | Afficher des variables avec leur nom (raccourci d'objet) |
console.table(pieces) | Afficher un tableau d'objets sous forme de tableau, une colonne par propriété |
console.warn('…') / console.error('…') | Avertissement en jaune, erreur en rouge, filtrables |
console.group('Démarrage') … console.groupEnd() | Regrouper des messages repliables |
console.time('chargement') … console.timeEnd('chargement') | Mesurer une durée |
console.count('rafraichir') | Compter combien de fois une ligne s'exécute |
console.assert(condition, '…') | N'afficher le message que si la condition est fausse |
console.table(pieces); // le stock en tableau lisible
console.log({ quantite, seuil }); // { quantite: 3, seuil: 4 } au lieu de « 3 4 »
Dans la console de Chrome, $0 désigne l'élément sélectionné dans l'onglet Éléments : pratique pour tester $0.textContent ou $0.classList.
Avant de livrer, retire les console.log de débogage : ils encombrent la console et peuvent afficher des données que l'utilisateur n'a pas à voir.
Lire un message d'erreur
Quand une erreur n'est pas interceptée, le script s'arrête et la console affiche une ligne rouge qui contient le type d'erreur, un message et l'endroit (fichier et ligne, cliquables). En dessous, la pile d'appels (stack trace) indique quelle fonction a appelé quelle autre : lis-la de haut en bas, la première ligne qui pointe vers ton code est en général celle à corriger.
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
at main.js:27:16
Les messages que tu rencontreras le plus souvent (formulation de Chrome, Edge et Node.js) :
| Message | Cause probable |
|---|---|
TypeError: Cannot read properties of null (reading 'addEventListener') | querySelector n'a rien trouvé : sélecteur faux, ou script exécuté trop tôt |
TypeError: Cannot read properties of undefined (reading 'ref') | Un élément de tableau qui n'existe pas (pieces[5]), un find sans résultat, une propriété mal orthographiée |
TypeError: Cannot set properties of null (setting 'textContent') | Même cause que la première, à l'écriture |
TypeError: pieces.map is not a function | La variable n'est pas un tableau : un objet, une chaîne JSON pas encore convertie, une promesse sans await |
TypeError: Assignment to constant variable. | Une variable const réaffectée : déclare-la avec let |
ReferenceError: valeurStok is not defined | Faute de frappe, variable hors de sa portée, import manquant |
ReferenceError: Cannot access 'x' before initialization | Variable let ou const lue avant sa déclaration |
SyntaxError: Unexpected token '{' | Parenthèse, accolade ou virgule mal placée : regarde la ligne indiquée et la précédente |
SyntaxError: Unexpected end of input | Une accolade ou une parenthèse jamais fermée |
SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON | json() a reçu une page HTML au lieu de JSON (chapitre 10) |
RangeError: Maximum call stack size exceeded | Une fonction s'appelle elle-même sans fin, ou deux fonctions s'appellent l'une l'autre |
Uncaught (in promise) … | Une promesse rompue que personne n'a attrapée : try…catch ou catch manquant |
Points d'arrêt et exécution pas à pas
Multiplier les console.log finit par être lent. Un point d'arrêt (breakpoint) met le programme en pause sur une ligne : tu vois alors la valeur de toutes les variables à cet instant, et tu avances instruction par instruction.
- Ouvre l'onglet Sources, retrouve ton fichier dans l'arborescence (ou
Ctrl+Ppour le chercher par son nom). - Clique sur le numéro de ligne : un marqueur bleu apparaît.
- Déclenche l'action (clique sur le bouton, recharge la page). L'exécution s'arrête avant cette ligne.
- Survole une variable pour voir sa valeur, ou regarde le panneau Portée (Scope) à droite.
- Avance avec les boutons de contrôle :
| Bouton | Raccourci | Effet |
|---|---|---|
| Reprendre | F8 | Continue jusqu'au prochain point d'arrêt |
| Pas à pas principal (step over) | F10 | Exécute la ligne, sans entrer dans les fonctions appelées |
| Pas à pas détaillé (step into) | F11 | Entre dans la fonction appelée sur la ligne |
| Pas à pas sortant (step out) | Maj + F11 | Termine la fonction en cours et revient à l'appelant |
Quelques variantes très utiles :
- Point d'arrêt conditionnel (clic droit sur le numéro de ligne) : ne s'arrête que si une condition est vraie, par exemple
p.ref === 'CHG-65'dans une boucle de 500 pièces. - Point de journalisation (logpoint) : affiche un message dans la console sans modifier ton code, ni pause.
- Expressions surveillées (Watch) : une expression recalculée à chaque pas, comme
etat.pieces.length. - Pause sur exception : arrête l'exécution à l'endroit précis où une erreur est levée, avant qu'elle ne remonte.
- L'instruction
debugger;dans ton code agit comme un point d'arrêt quand les outils de développement sont ouverts. Ne l'oublie pas avant de livrer.
Dans VS Code, tu peux poser les mêmes points d'arrêt directement dans l'éditeur : c'est le même mécanisme, utilisé aussi pour déboguer du Node.js.
L'onglet Réseau : inspecter les requêtes
Dès qu'un fetch est en jeu, l'onglet Réseau est le premier endroit où regarder. Filtre sur Fetch/XHR pour ne voir que les appels d'API, puis clique sur une requête :
| Section | Ce que tu y vérifies |
|---|---|
| En-têtes (Headers) | L'URL exacte, la méthode, le code de statut, les en-têtes envoyés (Content-Type, Authorization) et reçus |
| Charge utile (Payload) | Le corps envoyé : ton JSON est-il bien formé, les noms des champs sont-ils les bons ? |
| Aperçu, Réponse | Ce que le serveur a vraiment renvoyé : du JSON, ou une page d'erreur HTML ? |
| Minutage (Timing) | Le temps passé à attendre le serveur |
Trois réglages en haut de l'onglet servent à tester ton code dans des conditions difficiles :
- Désactiver le cache, pour être sûr de voir la dernière version de tes fichiers.
- Limitation de bande passante (throttling) : choisis « 3G » pour voir ton message de chargement, ou « Hors connexion » pour vérifier que l'erreur réseau est bien gérée.
- Conserver le journal (Preserve log) : garde les requêtes après un rechargement ou un envoi de formulaire.
Une erreur CORS n'apparaît pas dans la réponse (JavaScript n'y a pas accès) mais dans la console, avec la raison exacte : has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header…. La correction se fait sur le serveur (CORS).
Gérer les erreurs : try, catch et throw
Les outils de développement servent à trouver tes bugs. Mais certaines erreurs ne sont pas des bugs : le réseau tombe, l'utilisateur saisit n'importe quoi, le stockage est plein. Ton code doit les prévoir.
throw lève une erreur : l'exécution s'arrête et remonte la pile d'appels jusqu'au premier try…catch qui l'intercepte.
function retirer(piece, n) {
if (n > piece.quantite) {
throw new Error(`Stock insuffisant pour ${piece.ref} : ${piece.quantite} disponible(s)`);
}
return { ...piece, quantite: piece.quantite - n };
}
try {
const apres = retirer({ ref: 'RAM-16', quantite: 3 }, 5);
console.log(apres.quantite);
} catch (erreur) {
console.log(erreur.message); // Stock insuffisant pour RAM-16 : 3 disponible(s)
} finally {
console.log('Opération terminée'); // s'exécute dans tous les cas
}
Lève toujours un objet Error (ou d'une classe dérivée), jamais une simple chaîne (throw 'erreur') : un Error porte un nom, un message et la pile d'appels qui permet de retrouver son origine.
Les erreurs intégrées au langage ont des types différents, que tu peux tester avec instanceof ou erreur.name :
| Type | Levée quand… |
|---|---|
Error | Type générique, celui que tu lèves toi-même |
TypeError | Une opération sur une valeur du mauvais type (lire une propriété de null, appeler ce qui n'est pas une fonction) ; aussi l'échec réseau de fetch |
ReferenceError | Une variable qui n'existe pas |
SyntaxError | Du code ou du JSON mal écrit |
RangeError | Une valeur hors limites (new Array(-1), pile d'appels pleine) |
Tu peux créer tes propres types d'erreur avec une classe (chapitre 11) pour distinguer une erreur de validation, à montrer à l'utilisateur, d'une panne, à signaler autrement :
class ErreurValidation extends Error {
constructor(champ, message) {
super(message);
this.name = 'ErreurValidation';
this.champ = champ;
}
}
try {
throw new ErreurValidation('prix', 'Le prix est obligatoire');
} catch (erreur) {
if (erreur instanceof ErreurValidation) {
console.log(`Champ ${erreur.champ} : ${erreur.message}`); // Champ prix : Le prix est obligatoire
} else {
throw erreur; // pas une erreur prévue : on la laisse remonter
}
}
En dernier recours, deux écouteurs globaux attrapent ce qui a échappé à tous les try…catch. Ils ne réparent rien, mais ils permettent d'afficher un message générique ou d'envoyer l'erreur à un outil de suivi :
window.addEventListener('error', (event) => {
console.error('Erreur non interceptée :', event.message);
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Promesse rompue non interceptée :', event.reason);
});
Une méthode de débogage
- Reproduire le bug à coup sûr : quelles actions, quelles données ? Un bug qu'on ne sait pas reproduire ne se corrige pas.
- Lire le message d'erreur et la ligne indiquée, s'il y en a un.
- Localiser : pose un point d'arrêt avant l'endroit suspect, vérifie les valeurs. Si tout est juste, avance ; sinon, remonte.
- Formuler une hypothèse («
quantiteest une chaîne ») et la vérifier, plutôt que modifier le code au hasard. - Corriger, puis retester le cas qui plantait et les cas qui marchaient déjà. Un test automatique (chapitre 14) évite que le bug revienne.
Pour l'E6, garde une trace des bugs marquants et de leur résolution : expliquer comment tu as trouvé un problème est un excellent sujet d'échange avec le jury.
Exercices
Exercice 1 sur 4 Interpréter les messages Réussi À revoir
Pour chaque message, donne la cause la plus probable et la vérification à faire.
TypeError: Cannot read properties of null (reading 'addEventListener')à la lignedocument.querySelector('#form-pieces').addEventListener(…).TypeError: data.filter is not a functionjuste aprèsconst data = localStorage.getItem('gestock.pieces');.ReferenceError: rafraichir is not defineddansvue.js, alors que la fonction existe dansmain.js.Uncaught (in promise) TypeError: Failed to fetch.
Voir la réponseMasquer la réponse de l'exercice 1
- Aucun élément d'identifiant
form-pieces: vérifie l'orthographe dans le HTML (ici sans douteform-piece), puis que le script adeferoutype="module". getItemrenvoie une chaîne, et les chaînes n'ont pas de méthodefilter. Il manqueJSON.parse(…).- Chaque module a sa propre portée : une fonction de
main.jsn'est pas visible dansvue.js. Il faut l'exporter et l'importer, ou mieux, revoir le découpage pour quevue.jsn'ait pas besoin demain.js(les dépendances vont dans un seul sens). - Le
fetchn'a reçu aucune réponse (serveur arrêté, mauvaise adresse, CORS) et personne n'a intercepté l'erreur. Regarde l'onglet Réseau et la console pour la cause exacte, et ajoute untry…catchautour de l'appel.
Tu avais trouvé ?
Exercice 2 sur 4 Trouver le bug avec un point d'arrêt Réussi À revoir
Le résumé de Gestock affiche valeur du stock : NaN € depuis qu'une pièce a été ajoutée par le formulaire. Décris pas à pas comment tu localiserais le problème avec le débogueur, et la cause probable.
Voir la réponseMasquer la réponse de l'exercice 2
- Poser un point d'arrêt dans
valeurStock, sur la ligne dureduce, puis ajouter une pièce. - À la pause, regarder
piecesdans le panneau Portée (ouconsole.table(pieces)dans la console, qui fonctionne pendant la pause) : la nouvelle pièce a probablement unprixou unequantitequi n'est pas un nombre ('7,90',''ouNaN). - Un point d'arrêt conditionnel
Number.isNaN(total)dans lereducearrête l'exécution exactement sur la pièce fautive.
Cause probable : la saisie n'a pas été convertie en nombre (creerPiece oubliée, ou Number('7,90') qui donne NaN à cause de la virgule). Correction : convertir avec lireNombre et refuser NaN dans validerPiece ; puis ajouter un test qui vérifie que la saisie '7,90' donne bien 7.9.
Tu avais trouvé ?
Exercice 3 sur 4 Le catch qui cache tout Réussi À revoir
Que reproches-tu à ce code ? Réécris-le.
async function demarrer() {
try {
const pieces = await chargerPiecesApi('/api/pieces');
afficherPieces(tbody, pieces);
afficherResume(resume, pieces);
} catch (e) {}
}
Voir la réponseMasquer la réponse de l'exercice 3
Le catch vide avale toutes les erreurs : si le serveur est en panne, la page reste vide sans explication ; pire, une faute de frappe dans afficherPieces (un bug du développeur) est cachée aussi.
async function demarrer() {
let pieces;
try {
pieces = await chargerPiecesApi('/api/pieces');
} catch (erreur) {
resume.textContent = `Impossible de charger le stock : ${erreur.message}`;
console.error(erreur);
return;
}
afficherPieces(tbody, pieces);
afficherResume(resume, pieces);
}
Le try n'entoure plus que l'opération qui peut normalement échouer (l'appel réseau). L'erreur est montrée à l'utilisateur et gardée dans la console. Un bug d'affichage, lui, apparaîtra en rouge dans la console au lieu d'être masqué.
Tu avais trouvé ?
Exercice 4 sur 4 Lever la bonne erreur Réussi À revoir
Écris une fonction appliquerRemise(prix, taux) qui renvoie le prix remisé (prix × (1 − taux / 100)) et lève une RangeError si le taux n'est pas compris entre 0 et 50, une TypeError si le prix n'est pas un nombre. Écris ensuite l'appel qui affiche le message d'erreur dans la console.
Voir la réponseMasquer la réponse de l'exercice 4
function appliquerRemise(prix, taux) {
if (typeof prix !== 'number' || Number.isNaN(prix)) {
throw new TypeError(`Prix invalide : ${prix}`);
}
if (taux < 0 || taux > 50) {
throw new RangeError(`Taux hors limites (0 à 50) : ${taux}`);
}
return prix * (1 - taux / 100);
}
try {
console.log(appliquerRemise(89, 10)); // 80.10000000000001
console.log(appliquerRemise(89, 70));
} catch (erreur) {
console.log(`${erreur.name} : ${erreur.message}`); // RangeError : Taux hors limites (0 à 50) : 70
}
Le premier appel affiche 80.10000000000001 : le calcul est juste, c'est la représentation des décimaux (chapitre 2) ; on arrondit à l'affichage. Le second lève la RangeError, attrapée par le catch.
Tu avais trouvé ?