Exercices JavaScript corrigés : 20 exercices exécutés, des variables au fetch
L’essentiel en une minute
Ces 20 exercices JavaScript corrigés vont d’une phrase construite avec un template string à un petit programme en modules qui interroge une API, avec pour chacun un énoncé, un indice, la correction et la sortie réelle. Si vous découvrez le langage, lisez d’abord notre guide Apprendre JavaScript en 2026, qui couvre l’installation et la théorie : cette page sert à pratiquer.
Toutes les corrections ont été exécutées le 6 novembre 2026 avec Node.js 22.15.0 et Node.js 26.13.0 : chaque sortie affichée est celle du terminal, identique sur les deux versions. Les trois exercices sur le DOM ont été testés automatiquement dans Chromium 120, Chrome 156 et Edge 157, où les 14 premières corrections tournent aussi. Node.js 22.15.0 n’est pas la dernière 22.x (la 22.23.4 est sortie le 20 octobre 2026), mais aucune correction ne dépend d’un correctif récent.
Points clés
- 19 exercices en 5 niveaux plus un projet final : bases, fonctions, tableaux et chaînes, DOM et événements, puis fetch et modules.
- Chaque exercice suit le même format : énoncé, indice, correction, sortie attendue et point à retenir.
- Les sorties viennent d’une exécution réelle sous Node.js 22.15.0 et 26.13.0 ; Node 26 est la version LTS depuis le 28 octobre 2026.
- Une section reprend les erreurs les plus fréquentes avec le message exact affiché par Node.js ou le navigateur.
- MDN, freeCodeCamp et Exercism se disent gratuits sur leurs pages officielles ; Codewars vend un abonnement facultatif, Codewars Red.
Comment utiliser ces exercices JavaScript corrigés
Essayez seul, puis lisez l’indice, et seulement ensuite la correction : une correction lue trop tôt donne l’impression d’avoir compris sans laisser de trace.
Les exercices 1 à 14 tournent dans Node.js. Enregistrez le code dans un fichier, par exemple exercice01.js, puis lancez node exercice01.js. Ils tournent aussi dans un navigateur : chargés comme script d’une page dans Chromium, Chrome et Edge, ils ont donné les mêmes valeurs, sans erreur. Seule la présentation des tableaux et des objets diffère d’une console à l’autre, par exemple dans les outils de développement (F12). Les exercices 15 à 17 sont des pages HTML complètes : ouvrez le fichier dans le navigateur. Si le HTML vous est encore étranger, notre cours HTML CSS débutant pose les bases en une page.
Les exercices 18 à 20 utilisent fetch, global dans Node.js depuis la version 18 et stable depuis la version 21 selon la documentation de Node.js. Ils utilisent aussi await hors d’une fonction, ce qui demande un module : gardez l’extension .mjs.
Quelle version installer ? La page des versions de Node.js affiche la 26.13.0 comme « Latest LTS » le 6 novembre 2026 et précise que les applications en production ne devraient utiliser que des versions LTS actives ou en maintenance (notre traduction). Le calendrier officiel de nodejs/Release donne les dates de chaque branche.

Le tableau de progression
Les exercices vont du plus simple au plus complet. Les temps sont des estimations de l’auteur pour une première tentative, indice compris.
| N° | Exercice | Notion travaillée | Niveau | Temps indicatif |
|---|---|---|---|---|
| 1 | Fiche de présentation | let, const, template string, typeof |
1 | 10 min |
| 2 | Prix TTC | Conversion de types, toFixed |
1 | 10 min |
| 3 | Mention d’une note | if, else if |
1 | 10 min |
| 4 | Horaires du jour | switch, Date |
1 | 15 min |
| 5 | FizzBuzz | Boucle for, modulo |
2 | 15 min |
| 6 | Objectif d’épargne | Boucle while |
2 | 10 min |
| 7 | Convertir des températures | Fonctions, fonctions fléchées | 2 | 15 min |
| 8 | Compteur privé | Fermeture (closure) | 2 | 20 min |
| 9 | Panier d’achat | filter, map, reduce |
3 | 20 min |
| 10 | Fréquence des mots | split, reduce, Object.entries |
3 | 25 min |
| 11 | Profil utilisateur | Déstructuration, spread | 3 | 15 min |
| 12 | Palindrome | normalize, expressions régulières |
3 | 20 min |
| 13 | Créer un slug | Méthodes de chaînes | 3 | 15 min |
| 14 | Classement d’une classe | sort, localeCompare |
3 | 20 min |
| 15 | Compteur cliquable | DOM, addEventListener |
4 | 20 min |
| 16 | Liste de tâches | Création d’éléments, délégation | 4 | 40 min |
| 17 | Formulaire d’inscription | Événements input et submit |
4 | 40 min |
| 18 | Charger un utilisateur | fetch, async, await, response.ok |
5 | 30 min |
| 19 | Module de géométrie | export, import |
5 | 15 min |
| 20 | Bilan des tâches | Projet : modules, fetch, reduce |
Projet | 60 min |
Niveau 1 : variables, types et conditions
Ce niveau ne demande que des variables, quelques opérations et des conditions. Le guide MDN des littéraux de gabarits, le nom officiel des template strings, et celui de l’opérateur typeof suffisent.
Exercice 1 : fiche de présentation
Énoncé. Stockez un prénom, un âge et une ville. Ajoutez un an à l’âge, affichez une phrase de présentation, puis le type de cinq valeurs.
Indice. const pour ce qui ne change pas, let pour ce qui change. Un template string s’écrit entre accents graves et insère une valeur avec ${...}.
const prenom = "Léa";
let age = 24;
const ville = "Lyon";
age = age + 1; // anniversaire : let autorise la réaffectation
console.log(`Je m'appelle ${prenom}, j'ai ${age} ans et j'habite à ${ville}.`);
console.log(typeof prenom, typeof age, typeof true, typeof undefined, typeof null);
Sortie attendue :
Je m'appelle Léa, j'ai 25 ans et j'habite à Lyon.
string number boolean undefined object
À retenir. typeof null renvoie "object" : c’est une bizarrerie historique du langage, pas une erreur de votre code.
Exercice 2 : calculer un prix TTC
Énoncé. Un prix arrive sous forme de texte, "19.90", comme la valeur d’un champ de formulaire. Calculez le total TTC de 3 articles avec une TVA de 20 %.
Indice. + entre un texte et un nombre colle les deux. Convertissez d’abord avec Number().
const prixSaisi = "19.90"; // ce que renvoie la valeur d'un champ de formulaire
const quantite = 3;
const tauxTva = 0.2;
console.log(prixSaisi + quantite); // piège : + colle deux textes
const prixHt = Number(prixSaisi);
const totalTtc = prixHt * quantite * (1 + tauxTva);
console.log(`Total TTC : ${totalTtc.toFixed(2)} euros`);
console.log(Number("abc"), Number.isNaN(Number("abc")));
Sortie attendue :
19.903
Total TTC : 71.64 euros
NaN true
À retenir. toFixed(2) renvoie un texte arrondi pour l’affichage. Un texte non numérique donne NaN, à tester avec Number.isNaN().
Exercice 3 : mention d’une note
Énoncé. Pour une note sur 20, affichez « Très bien » à partir de 16, « Bien » à partir de 14, « Assez bien » à partir de 12, « Admis » à partir de 10, sinon « Non admis ». Refusez une note hors de 0 à 20.
Indice. Testez d’abord la validité, puis les seuils du plus haut au plus bas.
const note = 14.5;
if (note < 0 || note > 20 || Number.isNaN(note)) {
console.log("Note invalide");
} else if (note >= 16) {
console.log("Très bien");
} else if (note >= 14) {
console.log("Bien");
} else if (note >= 12) {
console.log("Assez bien");
} else if (note >= 10) {
console.log("Admis");
} else {
console.log("Non admis");
}
Sortie attendue :
Bien
À retenir. Seule la première branche vraie s’exécute : si vous testez note >= 10 en premier, tout le monde est « Admis ».
Exercice 4 : horaires du jour
Énoncé. Pour le 6 novembre 2026, affichez le jour en toutes lettres et les horaires d’un magasin : fermé le dimanche, 10 h à 13 h le samedi, 9 h à 18 h sinon.
Indice. getDay() renvoie 0 pour dimanche et 6 pour samedi. Attention : dans new Date(année, mois, jour), les mois commencent à 0.
const date = new Date(2026, 10, 6); // les mois commencent à 0 : 10 = novembre
const jour = date.getDay(); // 0 = dimanche, 6 = samedi
let horaires;
switch (jour) {
case 0:
horaires = "fermé";
break;
case 6:
horaires = "10 h à 13 h";
break;
default:
horaires = "9 h à 18 h";
}
const libelle = date.toLocaleDateString("fr-FR", { weekday: "long", day: "numeric", month: "long" });
console.log(`${libelle} : ${horaires}`);
Sortie attendue :
vendredi 6 novembre : 9 h à 18 h
À retenir. Sans break, l’exécution continue dans le case suivant. La page MDN de l’objet Date détaille le piège des mois.
Niveau 2 : boucles et fonctions
Ce niveau répète des instructions et les range dans des fonctions. Le guide MDN des fonctions fléchées et celui des fermetures accompagnent les exercices 7 et 8.
Exercice 5 : FizzBuzz
Énoncé. Pour les nombres de 1 à 15, affichez « Fizz » pour les multiples de 3, « Buzz » pour les multiples de 5, « FizzBuzz » pour les multiples des deux, et le nombre sinon, le tout sur une ligne.
Indice. Testez d’abord le cas le plus restrictif. Remplissez un tableau avec push, puis assemblez-le avec join(" ").
const resultats = [];
for (let i = 1; i <= 15; i++) {
if (i % 15 === 0) {
resultats.push("FizzBuzz");
} else if (i % 3 === 0) {
resultats.push("Fizz");
} else if (i % 5 === 0) {
resultats.push("Buzz");
} else {
resultats.push(i);
}
}
console.log(resultats.join(" "));
Sortie attendue :
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
À retenir. Avec i % 3 testé en premier, 15 affiche « Fizz » : la branche FizzBuzz n’est jamais atteinte.
Exercice 6 : objectif d’épargne
Énoncé. En versant 75 euros par mois, combien de mois faut-il pour atteindre 1 000 euros ?
Indice. Le nombre de tours n’est pas connu à l’avance : c’est le cas d’usage de while.
const objectif = 1000;
const versementMensuel = 75;
let solde = 0;
let mois = 0;
while (solde < objectif) {
solde += versementMensuel;
mois++;
}
console.log(`${mois} mois pour atteindre ${objectif} euros (solde : ${solde} euros)`);
Sortie attendue :
14 mois pour atteindre 1000 euros (solde : 1050 euros)
À retenir. Une boucle while doit modifier sa condition à chaque tour. Avec un versement de 0, celle-ci ne s’arrêterait jamais : ajoutez un garde-fou.
Exercice 7 : convertir des températures
Énoncé. Écrivez une fonction classique qui convertit des degrés Celsius en Fahrenheit, une fonction fléchée pour l’inverse, et une fonction arrondir avec une décimale par défaut.
Indice. F = C x 9 / 5 + 32. Un paramètre par défaut s’écrit decimales = 1. Une fonction se passe à map sans parenthèses.
function celsiusVersFahrenheit(celsius) {
return celsius * 9 / 5 + 32;
}
const fahrenheitVersCelsius = (f) => (f - 32) * 5 / 9;
const arrondir = (nombre, decimales = 1) =>
Math.round(nombre * 10 ** decimales) / 10 ** decimales;
console.log(celsiusVersFahrenheit(100));
console.log(arrondir(fahrenheitVersCelsius(98.6)));
console.log(arrondir(Math.PI, 3));
console.log([0, 20, 37].map(celsiusVersFahrenheit));
Sortie attendue :
212
37
3.142
[ 32, 68, 98.6 ]
À retenir. Une fonction fléchée sur une ligne renvoie sa valeur sans return. Avec des accolades, le return redevient obligatoire.
Exercice 8 : un compteur privé
Énoncé. Écrivez creerCompteur(depart) qui renvoie un objet avec incrementer, decrementer et reinitialiser. La valeur ne doit pas être accessible directement.
Indice. Une fonction définie dans une autre garde l’accès aux variables de celle-ci, même après son retour : c’est une fermeture.
function creerCompteur(depart = 0) {
let valeur = depart; // invisible depuis l'extérieur
return {
incrementer: () => ++valeur,
decrementer: () => --valeur,
reinitialiser: () => (valeur = depart),
};
}
const visites = creerCompteur();
const stock = creerCompteur(10);
visites.incrementer();
visites.incrementer();
stock.decrementer();
console.log(visites.incrementer(), stock.decrementer());
console.log(visites.reinitialiser(), stock.valeur);
Sortie attendue :
3 8
0 undefined
À retenir. Chaque appel à creerCompteur crée sa propre variable valeur. stock.valeur vaut undefined : la donnée est bien protégée. L’exercice 15 reprend cette logique dans une page.
Niveau 3 : tableaux, objets et chaînes
Ce niveau traite des séries de données. La page MDN de reduce et celle de l’affectation par décomposition, le nom français de la déstructuration, sont les deux références utiles.
Exercice 9 : le panier d’achat
Énoncé. À partir d’un panier, gardez les articles en stock, affichez une ligne par article avec son sous-total, puis le total.
Indice. filter garde, map transforme, reduce cumule. Donnez toujours une valeur de départ à reduce, ici 0.
const panier = [
{ nom: "Clavier", prix: 49.9, quantite: 1, enStock: true },
{ nom: "Souris", prix: 19.5, quantite: 2, enStock: true },
{ nom: "Écran", prix: 189, quantite: 1, enStock: false },
{ nom: "Câble USB-C", prix: 9.99, quantite: 3, enStock: true },
];
const disponibles = panier.filter((article) => article.enStock);
const lignes = disponibles.map(
(article) => `${article.nom} x${article.quantite} : ${(article.prix * article.quantite).toFixed(2)} euros`
);
const total = disponibles.reduce((somme, article) => somme + article.prix * article.quantite, 0);
console.log(lignes.join("\n"));
console.log(`Total : ${total.toFixed(2)} euros`);
Sortie attendue :
Clavier x1 : 49.90 euros
Souris x2 : 39.00 euros
Câble USB-C x3 : 29.97 euros
Total : 118.87 euros
À retenir. Sans valeur de départ, reduce sur un tableau vide lève TypeError: Reduce of empty array with no initial value. Pour de vrais montants, calculez en centimes entiers.
Exercice 10 : fréquence des mots
Énoncé. Comptez les mots d’une phrase sans tenir compte de la ponctuation ni des majuscules, puis affichez les trois plus fréquents.
Indice. split accepte une expression régulière. compteur[mot] ?? 0 vaut 0 quand le mot est absent.
const texte = "Le code court, le code clair. Un code clair se relit, un code court se teste.";
const mots = texte
.toLowerCase()
.split(/[^\p{L}]+/u)
.filter((mot) => mot.length > 0);
const frequences = mots.reduce((compteur, mot) => {
compteur[mot] = (compteur[mot] ?? 0) + 1;
return compteur;
}, {});
const top3 = Object.entries(frequences)
.sort((a, b) => b[1] - a[1])
.slice(0, 3);
console.log(mots.length, "mots");
console.log(top3);
Sortie attendue :
16 mots
[ [ 'code', 4 ], [ 'le', 2 ], [ 'court', 2 ] ]
À retenir. \p{L} désigne toute lettre Unicode, accents et ligatures compris : avec [^a-zà-ÿ], « cœur » serait coupé en c et ur, un résultat vérifié dans Node.js. Cinq mots sont à égalité à 2. La page MDN de sort indique que la spécification impose un tri stable depuis ECMAScript 2019 : l’ordre d’apparition est conservé.
Exercice 11 : profil utilisateur
Énoncé. Extrayez le nom, l’e-mail, la ville et un rôle par défaut d’un objet. Prenez le premier et le troisième élément d’un tableau, créez une copie promue administrateur, puis échangez deux variables.
Indice. La déstructuration accepte des valeurs par défaut (role = "lecteur") et des objets imbriqués. { ...objet } copie les propriétés.
const utilisateur = {
id: 7,
nom: "Inès",
email: "[email protected]",
adresse: { ville: "Nantes" },
};
const { nom, email, role = "lecteur", adresse: { ville } } = utilisateur;
console.log(nom, email, role, ville);
const [premier, , troisieme = "aucun"] = ["HTML", "CSS"];
console.log(premier, troisieme);
const promu = { ...utilisateur, role: "admin" };
console.log(utilisateur.role, promu.role);
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b);
Sortie attendue :
Inès [email protected] lecteur Nantes
HTML aucun
undefined admin
2 1
À retenir. L’objet d’origine n’est pas modifié. Mais la copie par spread est superficielle : promu.adresse et utilisateur.adresse restent le même objet.
Exercice 12 : palindrome avec accents
Énoncé. Écrivez estPalindrome(phrase) qui ignore la casse, les espaces, la ponctuation et les accents. Testez trois phrases.
Indice. normalize("NFD") sépare la lettre de son accent, que l’on retire ensuite avec une expression régulière. Une chaîne n’a pas de reverse : passez par un tableau.
function estPalindrome(phrase) {
const propre = phrase
.toLowerCase()
.normalize("NFD") // "é" devient "e" + accent séparé
.replace(/[\u0300-\u036f]/g, "") // on retire les accents
.replace(/[^a-z0-9]/g, ""); // puis espaces et ponctuation
return propre === [...propre].reverse().join("");
}
for (const phrase of ["Ésope reste ici et se repose", "Engage le jeu que je le gagne", "JavaScript"]) {
console.log(`${phrase} -> ${estPalindrome(phrase)}`);
}
Sortie attendue :
Ésope reste ici et se repose -> true
Engage le jeu que je le gagne -> true
JavaScript -> false
À retenir. Sans normalize, le « é » d’« Ésope » est supprimé par [^a-z0-9] avec la ponctuation : il reste soperesteicietserepose, qui n’est plus un palindrome, et le test renvoie false. La page MDN de normalize explique les formes NFC et NFD.
Exercice 13 : créer un slug
Énoncé. Transformez un titre en slug d’URL : minuscules, sans accents, mots séparés par des tirets, sans tiret au début ni à la fin.
Indice. Même départ que l’exercice 12, puis remplacez chaque suite de caractères non alphanumériques par un tiret.
const slugifier = (titre) =>
titre
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.trim()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
console.log(slugifier("Exercices JavaScript corrigés : 20 énoncés !"));
console.log(slugifier(" Ça marche, même avec des œufs ? "));
console.log("corrigés".length, "corrigés".normalize("NFD").length);
Sortie attendue :
exercices-javascript-corriges-20-enonces
ca-marche-meme-avec-des-ufs
8 9
À retenir. Deux surprises, toutes deux réelles : « œufs » devient « ufs », car « œ » est une ligature et non une lettre accentuée, et length compte 9 après NFD. Traitez « œ » et « æ » à part avec un replace.
Exercice 14 : classement d’une classe
Énoncé. Triez d’abord [10, 9, 1, 100] sans puis avec fonction de comparaison. Calculez ensuite la moyenne de chaque élève et affichez le classement, les ex aequo par ordre alphabétique.
Indice. Sans fonction de comparaison, sort compare des textes. localeCompare trie correctement les prénoms accentués.
console.log([10, 9, 1, 100].sort());
console.log([10, 9, 1, 100].sort((a, b) => a - b));
const classe = [
{ nom: "Omar", notes: [12, 15, 9] },
{ nom: "Élodie", notes: [16, 14, 18] },
{ nom: "Zoé", notes: [11, 13, 12] },
{ nom: "Bastien", notes: [16, 16, 16] },
];
const calculerMoyenne = (notes) => notes.reduce((s, n) => s + n, 0) / notes.length;
const classement = classe
.map(({ nom, notes }) => ({ nom, moyenne: calculerMoyenne(notes) }))
.sort((a, b) => b.moyenne - a.moyenne || a.nom.localeCompare(b.nom, "fr"));
for (const { nom, moyenne } of classement) {
console.log(`${nom.padEnd(8)} ${moyenne.toFixed(2)}`);
}
Sortie attendue :
[ 1, 10, 100, 9 ]
[ 1, 9, 10, 100 ]
Bastien 16.00
Élodie 16.00
Omar 12.00
Zoé 12.00
À retenir. MDN le confirme : par défaut, sort convertit les éléments en chaînes, d’où 100 avant 9. Et sort modifie le tableau d’origine.
Niveau 4 : le DOM et les événements
Ces trois exercices sont des pages HTML complètes : enregistrez-les en .html et ouvrez-les dans le navigateur. Le script est placé en fin de <body>, après les éléments qu’il manipule. La page MDN d’addEventListener décrit les événements utilisés.
Comment nous les avons testés. Un script Playwright 1.40 a cliqué, saisi et soumis chaque page dans Chromium 120, Chrome 156 et Edge 157, puis relu son contenu : résultats identiques dans les trois, sans erreur JavaScript.
Exercice 15 : un compteur cliquable
Énoncé. Affichez une valeur et trois boutons : +1, -1 et remise à zéro. Une valeur négative s’affiche en rouge.
Indice. Récupérez les éléments avec document.querySelector, puis réagissez au clic avec addEventListener("click", ...). Modifiez le texte avec textContent.
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Compteur</title>
</head>
<body>
<p>Valeur : <output id="valeur">0</output></p>
<button type="button" id="moins">-1</button>
<button type="button" id="plus">+1</button>
<button type="button" id="zero">Remettre à zéro</button>
<script>
const affichage = document.querySelector("#valeur");
let compteur = 0;
function afficher() {
affichage.textContent = compteur;
affichage.style.color = compteur < 0 ? "crimson" : "";
}
document.querySelector("#plus").addEventListener("click", () => {
compteur++;
afficher();
});
document.querySelector("#moins").addEventListener("click", () => {
compteur--;
afficher();
});
document.querySelector("#zero").addEventListener("click", () => {
compteur = 0;
afficher();
});
</script>
</body>
</html>
Résultat vérifié : après 3 clics sur +1, la page affiche 3 ; après 5 clics sur -1, elle affiche -2 en rouge (rgb(220, 20, 60)) ; le bouton de remise à zéro affiche 0.
À retenir. Une seule fonction afficher met la page à jour, ce qui garde l’état et l’affichage synchronisés.
Exercice 16 : une liste de tâches
Énoncé. Un formulaire ajoute une tâche à une liste. Chaque tâche a un bouton « Fait », qui la barre, et un bouton « Supprimer ». Un texte indique le nombre de tâches restantes. Une saisie vide ou faite d’espaces est ignorée.
Indice. Bloquez le rechargement avec preventDefault(). Créez les éléments avec createElement et posez un seul écouteur sur la liste : c’est la délégation d’événements.
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Liste de tâches</title>
<style>.faite span { text-decoration: line-through; opacity: .6; }</style>
</head>
<body>
<form id="formulaire">
<label for="nouvelle">Nouvelle tâche</label>
<input id="nouvelle">
<button>Ajouter</button>
</form>
<ul id="liste"></ul>
<p id="reste">0 tâche restante</p>
<script>
const formulaire = document.querySelector("#formulaire");
const champ = document.querySelector("#nouvelle");
const liste = document.querySelector("#liste");
const reste = document.querySelector("#reste");
function mettreAJourCompteur() {
const n = liste.querySelectorAll("li:not(.faite)").length;
reste.textContent = `${n} tâche${n > 1 ? "s" : ""} restante${n > 1 ? "s" : ""}`;
}
formulaire.addEventListener("submit", (evenement) => {
evenement.preventDefault(); // pas de rechargement de la page
const titre = champ.value.trim();
if (titre === "") return;
const li = document.createElement("li");
const texte = document.createElement("span");
texte.textContent = titre; // textContent, jamais innerHTML
const boutonFait = document.createElement("button");
boutonFait.textContent = "Fait";
boutonFait.dataset.action = "basculer";
const boutonSuppr = document.createElement("button");
boutonSuppr.textContent = "Supprimer";
boutonSuppr.dataset.action = "supprimer";
li.append(texte, " ", boutonFait, " ", boutonSuppr);
liste.append(li);
champ.value = "";
champ.focus();
mettreAJourCompteur();
});
// Un seul écouteur pour tous les boutons, même ceux créés plus tard
liste.addEventListener("click", (evenement) => {
const bouton = evenement.target.closest("button");
if (!bouton) return;
const li = bouton.closest("li");
if (bouton.dataset.action === "basculer") li.classList.toggle("faite");
if (bouton.dataset.action === "supprimer") li.remove();
mettreAJourCompteur();
});
</script>
</body>
</html>
Résultat vérifié : trois tâches ajoutées avec la touche Entrée affichent « 3 tâches restantes », puis « 2 tâches restantes » après un clic sur « Fait » (texte barré), puis « 1 tâche restante » après une suppression. Une saisie vide ou de trois espaces n’ajoute rien, sans bulle d’erreur du navigateur, et l’URL ne change pas. La tâche <img src=x onerror=alert(1)> s’affiche comme du texte : aucune image n’est créée.
À retenir. Le champ n’a pas l’attribut required : avec lui, le navigateur afficherait sa propre bulle sur un envoi vide, alors qu’ici le test titre === "" suffit. textContent affiche le texte tel quel ; innerHTML l’interpréterait comme du HTML, une porte ouverte à l’injection de code.
Exercice 17 : valider un formulaire
Énoncé. Un formulaire d’inscription demande un e-mail, un mot de passe de 8 caractères minimum et une confirmation. Affichez un message sous chaque champ pendant la saisie, et vérifiez tout à l’envoi.
Indice. L’attribut novalidate désactive les bulles du navigateur pour afficher vos propres messages. Un objet de règles, une fonction par champ, évite de répéter le code.
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Inscription</title>
<style>.erreur { color: crimson; } input[aria-invalid="true"] { border-color: crimson; }</style>
</head>
<body>
<form id="inscription" novalidate>
<label for="email">E-mail</label>
<input id="email" type="email" required aria-describedby="email-erreur">
<p class="erreur" id="email-erreur" aria-live="polite"></p>
<label for="mdp">Mot de passe (8 caractères minimum)</label>
<input id="mdp" type="password" required aria-describedby="mdp-erreur">
<p class="erreur" id="mdp-erreur" aria-live="polite"></p>
<label for="confirmation">Confirmation</label>
<input id="confirmation" type="password" required aria-describedby="confirmation-erreur">
<p class="erreur" id="confirmation-erreur" aria-live="polite"></p>
<button>Créer le compte</button>
<p id="resultat" aria-live="polite"></p>
</form>
<script>
const regles = {
email: (v) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? "" : "Adresse e-mail invalide."),
mdp: (v) => (v.length >= 8 ? "" : `Encore ${8 - v.length} caractère(s).`),
confirmation: (v) =>
v === ""
? "Confirmez le mot de passe."
: v === document.querySelector("#mdp").value
? ""
: "Les mots de passe ne correspondent pas.",
};
function verifier(id) {
const champ = document.querySelector(`#${id}`);
// On nettoie l'e-mail, jamais un mot de passe : une espace y compte
const valeur = id === "email" ? champ.value.trim() : champ.value;
const message = regles[id](valeur);
document.querySelector(`#${id}-erreur`).textContent = message;
champ.setAttribute("aria-invalid", message !== "");
return message === "";
}
const formulaire = document.querySelector("#inscription");
const confirmation = document.querySelector("#confirmation");
// Vérification en direct pendant la saisie
formulaire.addEventListener("input", (evenement) => {
verifier(evenement.target.id);
// Changer le mot de passe peut rendre la confirmation juste ou fausse
if (evenement.target.id === "mdp" && confirmation.value !== "") verifier("confirmation");
});
formulaire.addEventListener("submit", (evenement) => {
evenement.preventDefault();
const resultats = Object.keys(regles).map(verifier); // tout vérifier, pas seulement le premier
const valide = resultats.every(Boolean);
document.querySelector("#resultat").textContent = valide
? "Compte prêt à être envoyé au serveur."
: "Corrigez les champs en rouge.";
});
</script>
</body>
</html>
Résultat vérifié : un envoi à vide affiche « Adresse e-mail invalide. », « Encore 8 caractère(s). », « Confirmez le mot de passe. » et « Corrigez les champs en rouge. ». La saisie de lea@exemple et secret affiche « Encore 2 caractère(s). ». Deux mots de passe différents affichent « Les mots de passe ne correspondent pas. », et ce message revient si l’on modifie le mot de passe après la confirmation. Avec des valeurs correctes, y compris un mot de passe entouré d’espaces saisi deux fois à l’identique, les messages disparaissent et la page affiche « Compte prêt à être envoyé au serveur. ». Chaque champ est relié à son message par aria-describedby, que lisent les lecteurs d’écran.
À retenir. La validation dans le navigateur aide l’utilisateur mais ne protège rien : le serveur doit refaire tous les contrôles. Ne nettoyez jamais un mot de passe avec trim() : une espace en fait partie. La règle de confirmation traite d’abord le champ vide, avec son propre message, sinon deux champs vides seraient jugés égaux.
Niveau 5 : fetch, async/await et modules
Ce niveau sort du fichier unique : il interroge une API et découpe le code. Si le vocabulaire des API vous manque, notre article API, c’est quoi ? explique requêtes, statuts et JSON.
Exercice 18 : charger un utilisateur avec fetch
Énoncé. Avec l’API gratuite JSONPlaceholder, qui se présente comme « free fake REST API » sans inscription, chargez les utilisateurs 1 et 999 et affichez nom, e-mail et ville. Gérez l’erreur HTTP et l’erreur réseau.
Indice. fetch ne rejette pas sa promesse sur une réponse 404. Le guide MDN Utiliser l’API Fetch le dit : il faut vérifier le statut avant de lire le corps. Testez response.ok et levez une erreur vous-même.
// exercice18.mjs
const API = "https://jsonplaceholder.typicode.com";
async function chargerUtilisateur(id) {
const reponse = await fetch(`${API}/users/${id}`);
if (!reponse.ok) {
// fetch ne rejette pas sur une erreur HTTP : à nous de vérifier
throw new Error(`HTTP ${reponse.status} pour l'utilisateur ${id}`);
}
return reponse.json();
}
async function afficher(id) {
try {
const { name, email, address: { city } } = await chargerUtilisateur(id);
console.log(`${name} <${email}>, ${city}`);
} catch (erreur) {
console.log(`Erreur : ${erreur.message}`);
}
}
await afficher(1);
await afficher(999);
try {
await fetch("https://api.exemple.invalid/users/1"); // domaine qui n'existe pas
} catch (erreur) {
console.log(`${erreur.name} : ${erreur.message}`);
}
Sortie attendue :
Leanne Graham <[email protected]>, Gwenborough
Erreur : HTTP 404 pour l'utilisateur 999
TypeError : fetch failed
À retenir. Une 404 arrive avec ok à false ; seule une panne réseau rejette la promesse. Dans les trois navigateurs testés, la même requête donne 200 et ok à true pour l’utilisateur 1, 404 et false pour le 999, mais le message réseau diffère : TypeError: Failed to fetch.
Exercice 19 : un module de géométrie
Énoncé. Créez geometrie.mjs qui exporte une constante, une fonction nommée et une fonction par défaut. Importez-les dans main.mjs de deux façons.
Indice. Un seul export default par module, importé sans accolades ; les exports nommés s’importent entre accolades. import * as regroupe tout dans un objet.
// geometrie.mjs
export const PI_ARRONDI = 3.14;
export function aireRectangle(largeur, hauteur) {
return largeur * hauteur;
}
export default function aireCercle(rayon) {
return Math.PI * rayon ** 2;
}
// main.mjs
import aireCercle, { aireRectangle, PI_ARRONDI } from "./geometrie.mjs";
import * as geo from "./geometrie.mjs";
console.log(aireRectangle(4, 2.5));
console.log(aireCercle(2).toFixed(2));
console.log(PI_ARRONDI, Object.keys(geo));
Sortie attendue de node main.mjs :
10
12.57
3.14 [ 'PI_ARRONDI', 'aireRectangle', 'default' ]
À retenir. Importer un nom qui n’existe pas arrête tout avant la première ligne : SyntaxError: The requested module './geometrie.mjs' does not provide an export named 'perimetre'. Dans le navigateur, le guide MDN des modules prévient qu’une page ouverte en file:// provoque des erreurs CORS : servez-la avec un serveur local.
Projet final : le bilan des tâches
Le dernier exercice réunit modules, fetch, déstructuration et reduce dans un programme en trois fichiers.
Exercice 20 : bilan des tâches en ligne de commande
Énoncé. JSONPlaceholder expose 200 tâches (/todos) et 10 utilisateurs (/users). Chargez les deux en parallèle, affichez le taux global de tâches terminées, puis le podium des trois utilisateurs les plus avancés. Une erreur doit afficher un message clair et un code de sortie non nul.
Indice. Promise.all lance les deux requêtes en même temps. ??= initialise une entrée seulement si elle n’existe pas. Une Map relie l’identifiant au nom.
// api.mjs
const API = "https://jsonplaceholder.typicode.com";
export async function chargerJson(chemin) {
const reponse = await fetch(`${API}${chemin}`);
if (!reponse.ok) {
throw new Error(`HTTP ${reponse.status} sur ${chemin}`);
}
return reponse.json();
}
// stats.mjs
export function bilanParUtilisateur(taches) {
const bilan = taches.reduce((acc, { userId, completed }) => {
acc[userId] ??= { total: 0, faites: 0 };
acc[userId].total++;
if (completed) acc[userId].faites++;
return acc;
}, {});
return Object.entries(bilan).map(([userId, { total, faites }]) => ({
userId: Number(userId),
total,
faites,
taux: Math.round((faites / total) * 100),
}));
}
// main.mjs
import { chargerJson } from "./api.mjs";
import { bilanParUtilisateur } from "./stats.mjs";
try {
const [taches, utilisateurs] = await Promise.all([
chargerJson("/todos"),
chargerJson("/users"),
]);
const noms = new Map(utilisateurs.map(({ id, name }) => [id, name]));
const faites = taches.filter((t) => t.completed).length;
console.log(`${taches.length} tâches, ${faites} terminées (${Math.round((faites / taches.length) * 100)} %)`);
const podium = bilanParUtilisateur(taches)
.sort((a, b) => b.taux - a.taux || a.userId - b.userId)
.slice(0, 3);
podium.forEach(({ userId, faites, total, taux }, i) => {
console.log(`${i + 1}. ${noms.get(userId)} : ${faites}/${total} (${taux} %)`);
});
} catch (erreur) {
console.error("Impossible de produire le bilan :", erreur.message);
process.exitCode = 1;
}
Sortie attendue de node main.mjs :
200 tâches, 90 terminées (45 %)
1. Chelsey Dietrich : 12/20 (60 %)
2. Clementina DuBuque : 12/20 (60 %)
3. Leanne Graham : 11/20 (55 %)
À retenir. stats.mjs ne fait aucun appel réseau : il se teste avec un tableau écrit à la main. Pour aller plus loin, affichez le bilan dans une page HTML en réutilisant l’exercice 16, puis versionnez le projet en suivant notre guide Git et GitHub pour débutant. Le typage de TypeScript aurait signalé une faute de frappe sur completed avant l’exécution.
Les erreurs du débutant et leurs vrais messages
Ces messages ont été produits le 6 novembre 2026 par Node.js 22.15.0 et 26.13.0, à l’identique, sauf la dernière ligne, obtenue dans Chromium, Chrome et Edge : le navigateur signale l’exception comme « Uncaught », c’est-à-dire non interceptée, d’où ce préfixe dans la console. Lisez d’abord la dernière ligne d’une erreur : elle donne le type et la cause.
| Code fautif | Message exact | Correction |
|---|---|---|
utilisateur.adresse.ville sans adresse |
TypeError: Cannot read properties of undefined (reading 'ville') |
utilisateur.adresse?.ville renvoie undefined |
const taux = 0.2; taux = 0.055; |
TypeError: Assignment to constant variable. |
let si la valeur doit changer |
console.log(prix); let prix = 10; |
ReferenceError: Cannot access 'prix' before initialization |
Déclarer avant d’utiliser |
notes.map(...) sur un objet |
TypeError: notes.map is not a function |
Object.values(notes).map(...) |
await dans une fonction sans async |
SyntaxError: await is only valid in async functions and the top level bodies of modules |
Ajouter async devant function |
reponse.json() sans await avant fetch |
TypeError: reponse.json is not a function |
const reponse = await fetch(url) |
Script dans <head> qui cherche un bouton |
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener') |
Script en fin de <body> ou attribut defer |
L’oubli de await est le plus sournois, car rien ne plante tout de suite. console.log(reponse) affiche Promise { <pending> } et reponse.ok vaut undefined : l’erreur n’apparaît qu’à la ligne suivante.
== contre ===. Le double égal convertit les types avant de comparer, comme le rappelle la page MDN de l’égalité stricte. Exécuté dans Node.js :
console.log(0 == "", "1" == 1, null == undefined, [] == false);
console.log(0 === "", "1" === 1, null === undefined, NaN === NaN, Number.isNaN(NaN));
true true true true
false false false false true
Utilisez === partout. NaN n’est égal à rien, pas même à lui-même : testez-le avec Number.isNaN().
Les nombres à virgule. 0.1 + 0.2 affiche 0.30000000000000004 et 0.1 + 0.2 === 0.3 vaut false. Les opérateurs se comportent aussi différemment avec du texte : "5" * 2 donne 10, "5" + 2 donne "52" et "5" - 2 donne 3. Notre guide Apprendre JavaScript revient sur var et sur this, deux autres pièges classiques.
Où s’entraîner en ligne gratuitement
Après ces 20 exercices, quatre plateformes prennent le relais. Nous avons vérifié leurs conditions le 6 novembre 2026, sans créer de compte.
| Plateforme | Langue | Gratuit ? | Ce qu’on y trouve |
|---|---|---|---|
| MDN Web Docs | Français | Oui, sans compte | Cours « Script dynamique avec JavaScript » et pages « Testez vos compétences » |
| freeCodeCamp | Anglais | Oui, tout le contenu | Cours, projets et certifications |
| Exercism, piste JavaScript | Anglais | Oui, mentorat compris | 159 exercices regroupés en 37 concepts |
| Codewars | Anglais | Compte de base sans prix affiché ; abonnement Red payant et facultatif | Kata (petits exercices) rédigés par la communauté |
MDN. Le module Script dynamique avec JavaScript, traduit en français et publié sous licence Creative Commons, propose une page de test par notion, par exemple les tableaux.
freeCodeCamp. Sa FAQ l’affirme : « Every aspect of freeCodeCamp is 100% free », cours, projets et certifications compris. L’organisation se présente comme une association américaine de type 501(c)(3).
Exercism. La page de la piste JavaScript annonce 159 exercices et 37 concepts, « 100% free for everyone », avec un mentorat assuré par des bénévoles.
Codewars. Le site propose de petits exercices appelés « kata ». Sa documentation décrit une inscription par e-mail ou compte GitHub sans mentionner de paiement, mais aucune page officielle lue ne dit en toutes lettres que l’entraînement est gratuit. La page Codewars Red vend un abonnement à 5 dollars par mois, 24 dollars par semestre ou 40 dollars par an, pour des statistiques détaillées, l’absence de publicité, un badge et l’accès aux fonctions en bêta.
Pratiquer JavaScript reste un bon investissement. Dans l’enquête Stack Overflow 2026, JavaScript est le langage le plus utilisé : 62,0 % des 14 099 personnes qui ont répondu à cette question disent l’avoir utilisé (Stack Overflow Developer Survey 2026, consulté le 6 novembre 2026). La page parle d’une première place « for the 12th year in a row », mais l’édition 2023 évoquait déjà une onzième année : nous ne reprenons pas ce décompte.

Une fois ces bases acquises, un framework devient accessible : notre comparatif React vs Vue aide à choisir. Et si vous venez d’un autre langage, nos exercices Python débutant suivent le même format.
Ce que nous n’avons pas pu vérifier
Les points suivants restent ouverts :
- Firefox et Safari. Les pages DOM ont été testées dans des navigateurs à moteur Chromium seulement : Chromium 120 fourni avec Playwright 1.40, une version ancienne, Chrome 156.0.8078.73 et Edge 157.0.4341.42. Ces deux dernières sont des versions du canal stable, mais Chrome 158, diffusé depuis le 4 novembre 2026, n’était pas encore installé sur le poste de test.
- Le décompte de Stack Overflow. L’édition 2026 écrit que JavaScript est premier « for the 12th year in a row » ; celle de 2023 parlait déjà d’« eleventh year in a row ». Le pourcentage de 62,0 % n’est pas en cause.
- Les autres versions de Node.js. Seules les versions 22.15.0 et 26.13.0 ont été utilisées. Node 24 n’a pas été testé.
- Les données de JSONPlaceholder. Ce sont de fausses données fixes ; si le service les modifie, les sorties des exercices 18 et 20 changeront.
- Le programme de freeCodeCamp et l’offre gratuite de Codewars. Les pages de cours de freeCodeCamp se chargent en JavaScript et n’ont pas été lues en détail. La page d’abonnement de Codewars ne décrit pas le compte gratuit ; nous n’avons pas créé de compte.
- Les temps indicatifs. Ce sont des estimations de l’auteur, pas des mesures.
Questions fréquentes
Existe-t-il une version PDF de ces exercices JavaScript corrigés ?
Pas de PDF à télécharger. Pour une version papier, imprimez la page depuis le navigateur (Ctrl+P, « Enregistrer au format PDF »). Mais un exercice se fait en exécutant du code : gardez un fichier par exercice.
Faut-il Node.js ou un navigateur pour faire ces exercices ?
Les deux sont gratuits. Les exercices 1 à 14 marchent dans Node.js et aussi dans le navigateur, chargés dans une page ou collés dans la console (F12) ; nous les avons testés chargés dans une page. Les exercices 15 à 17 demandent un navigateur ; les exercices 18 à 20 ont été écrits pour Node.js 22 ou plus récent.
Combien de temps faut-il pour faire les 20 exercices ?
Le total de nos estimations est d’environ sept heures, projet compris. Étalez ces exercices sur deux à trois semaines, un ou deux par jour, et refaites trois jours plus tard ceux qui vous ont posé problème.
Que faire après ces exercices ?
Construisez un petit projet personnel et publiez-le sur GitHub. Ensuite, deux suites logiques : TypeScript pour typer votre code, puis un framework comme React ou Vue.
À propos de l’auteur
Abdelilah Ourti est ingénieur IA et architecte logiciel. Il conçoit et déploie des applications web et des systèmes d’IA, et publie sur NeurauTech des guides et des comparatifs destinés aux développeurs francophones. Ses autres articles sont réunis sur sa page auteur.
Méthode. Les exercices ont été écrits pour cet article. Les corrections 1 à 14 et 18 à 20 ont été exécutées le 6 novembre 2026 avec Node.js 22.15.0 et 26.13.0, et leur sortie copiée telle quelle. Les messages du tableau d’erreurs, les comparaisons == et ===, 0.1 + 0.2, les conversions de "5", l’erreur de reduce sur un tableau vide, l’export manquant de l’exercice 19 et l’oubli de await (Promise { <pending> }, puis reponse.ok à undefined) viennent eux aussi d’exécutions sous ces deux versions.
Les exercices 15 à 17, l’erreur sur null, les statuts de fetch et le message « Failed to fetch » dans le navigateur, ainsi que les corrections 1 à 14, ont été vérifiés par des scripts Playwright dans Chromium 120, Chrome 156 et Edge 157, versions du canal stable pour les deux derniers.
Les références techniques viennent de MDN et de la documentation de Node.js ; les chiffres, de l’enquête Stack Overflow 2026. Les conditions des plateformes ont été lues sur leurs pages officielles. Aucun lien n’est affilié.
Une erreur dans une correction ? Écrivez à [email protected] : l’article sera corrigé et la date de mise à jour modifiée.
Dernière mise à jour : 6 novembre 2026.
Image : Markus Spiske (Unsplash License). Graphiques : NeurauTech, d’après l’enquête Stack Overflow 2026 et le calendrier de Node.js.
