Leçon 12 / 15

Afficher les états et traiter les erreurs réseau

25 min JavaScript — DOM, événements et asynchrone Gratuit

Objectif : Présenter un état de chargement, un résultat ou un message d’échec selon l’issue de la requête.

Une requête peut échouer sans que la page explique ce qui se passe : l’utilisateur clique, puis ne voit aucun résultat. Vous allez rendre son déroulement visible et traiter séparément les erreurs réseau et les réponses HTTP refusées.

Vous avez déjà vu comment attendre fetch et lire une réponse JSON avec async et await. Cette leçon ajoute le traitement des erreurs et un retour visuel, sans encore intégrer les tâches distantes à l’état du gestionnaire.

1. Distinguer les erreurs réseau et HTTP

fetch rejette sa promesse lors de certains problèmes de communication, par exemple une absence de connexion. En revanche, une réponse HTTP comme 404 ou 500 est une réponse reçue : vous devez vérifier response.ok et déclencher vous-même une erreur si la réponse n’est pas réussie.

Pour que l’interface puisse afficher un échec HTTP comme un échec réseau, la fonction de chargement ci-dessous lance une erreur si le statut HTTP n’est pas réussi. Elle convertit aussi les données reçues au format attendu par l’exercice, mais ne les ajoute pas à la liste affichée.

2. Annoncer le chargement et son résultat

Le paragraphe data-message sert de retour accessible et visible, tandis que le bouton data-charger déclenche la requête. Pendant l’attente, le bouton est désactivé pour éviter les demandes répétées ; le bloc finally le réactive, que l’opération réussisse ou échoue.

Voici l’état complet de js/main.js pour cette étape. Il conserve la restauration défensive et le stockage versionné des leçons précédentes ; les tâches sont maintenues en mémoire, mais leur rendu et leur persistance ne sont pas modifiés par cette démonstration.

index.htmlHTML
<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Gestionnaire de tâches</title>
  <link rel="stylesheet" href="style.css">
  <script type="module" src="js/main.js"></script>
</head>
<body class="atelier-page">
  <header class="atelier-entete">
    <h1>Gestionnaire de tâches</h1>
    <p>Organisez vos tâches et suivez votre progression.</p>
  </header>

  <main class="atelier-principal">
    <section class="atelier-progression" aria-labelledby="titre-progression">
      <h2 id="titre-progression">Progression</h2>
      <p><span data-progression-texte>0 tâche terminée sur 0</span></p>
      <progress class="atelier-progression__barre" data-progression max="100" value="0">0%</progress>
    </section>

    <form class="atelier-formulaire" data-formulaire>
      <label for="nouvelle-tache">Nouvelle tâche</label>
      <input class="atelier-champ" id="nouvelle-tache" name="titre" type="text" required>
      <button class="atelier-bouton" type="submit">Ajouter</button>
    </form>

    <div class="atelier-actions">
      <div class="atelier-filtres" role="group" aria-label="Filtrer les tâches">
        <button class="atelier-bouton" type="button" data-filtre="toutes" aria-pressed="true">Toutes</button>
        <button class="atelier-bouton" type="button" data-filtre="actives" aria-pressed="false">À faire</button>
        <button class="atelier-bouton" type="button" data-filtre="terminees" aria-pressed="false">Terminées</button>
      </div>
      <button class="atelier-bouton" type="button" data-charger>Charger des tâches</button>
    </div>

    <p class="atelier-message" data-message role="status" aria-live="polite"></p>
    <ul class="atelier-liste" data-liste aria-label="Liste des tâches"></ul>

    <template data-modele-tache>
      <li class="atelier-tache" data-id="">
        <input type="checkbox" data-action="basculer">
        <span data-titre></span>
        <button class="atelier-bouton" type="button" data-action="supprimer">Supprimer</button>
      </li>
    </template>
  </main>
</body>
</html>
Aperçu du rendu
Chargement de l'aperçu…
style.cssCSS
:root {
  --atelier-couleur-texte: #202124;
  --atelier-couleur-fond: #f5f6fa;
  --atelier-couleur-accent: #3157c8;
  --atelier-espace-petit: 0.5rem;
  --atelier-espace-moyen: 1rem;
  --atelier-espace-grand: 2rem;
  --atelier-rayon: 0.5rem;
  --atelier-police: system-ui, sans-serif;
  --atelier-largeur-page: 48rem;
  --atelier-ombre: 0 0.125rem 0.5rem #20212422;
}

body {
  margin: 0;
  padding: var(--atelier-espace-grand);
  color: var(--atelier-couleur-texte);
  background: var(--atelier-couleur-fond);
  font-family: var(--atelier-police);
}

.atelier-entete,
.atelier-principal {
  width: min(100%, var(--atelier-largeur-page));
  margin: 0 auto var(--atelier-espace-grand);
}

.atelier-principal {
  padding: var(--atelier-espace-grand);
  border-radius: var(--atelier-rayon);
  box-shadow: var(--atelier-ombre);
}

.atelier-formulaire,
.atelier-actions,
.atelier-filtres,
.atelier-tache {
  display: flex;
  gap: var(--atelier-espace-moyen);
  align-items: center;
}

.atelier-formulaire,
.atelier-actions {
  margin: var(--atelier-espace-grand) 0;
}

.atelier-liste {
  padding: 0;
  list-style: none;
}

.atelier-tache {
  margin: var(--atelier-espace-petit) 0;
  padding: var(--atelier-espace-moyen);
  border-radius: var(--atelier-rayon);
  box-shadow: var(--atelier-ombre);
}

.atelier-champ {
  flex: 1;
  padding: var(--atelier-espace-petit);
}

.atelier-bouton {
  padding: var(--atelier-espace-petit) var(--atelier-espace-moyen);
  border: 0;
  border-radius: var(--atelier-rayon);
  color: var(--atelier-couleur-fond);
  background: var(--atelier-couleur-accent);
  cursor: pointer;
}

.atelier-bouton:disabled {
  cursor: wait;
  opacity: 0.6;
}

.atelier-progression__barre {
  width: 100%;
}

.atelier-message {
  min-height: 1.5em;
}
Aperçu du rendu
Chargement de l'aperçu…

Aperçu appliqué au document HTML de cette leçon : la règle CSS est injectée dans la page pour voir son effet réel.

js/main.jsJavaScript
const STORAGE_KEY = "gestionnaire-taches";
const STORAGE_VERSION = 1;
const API_URL = "https://jsonplaceholder.typicode.com/todos?_limit=5";

const formulaire = document.querySelector("[data-formulaire]");
const champTitre = document.querySelector("[name='titre']");
const liste = document.querySelector("[data-liste]");
const modeleTache = document.querySelector("[data-modele-tache]");
const message = document.querySelector("[data-message]");
const boutonCharger = document.querySelector("[data-charger]");
const barreProgression = document.querySelector("[data-progression]");
const texteProgression = document.querySelector("[data-progression-texte]");
const filtres = document.querySelectorAll("[data-filtre]");

if (!formulaire || !champTitre || !liste || !modeleTache || !message || !boutonCharger || !barreProgression || !texteProgression || filtres.length === 0) {
  throw new Error("Un élément requis du gestionnaire est absent de la page.");
}

let taches = restaurerTaches();
let filtreActif = "toutes";

function validerTache(valeur) {
  return Boolean(
    valeur &&
    typeof valeur === "object" &&
    typeof valeur.id === "string" &&
    typeof valeur.titre === "string" &&
    typeof valeur.terminee === "boolean"
  );
}

function restaurerTaches() {
  try {
    const valeurStockee = localStorage.getItem(STORAGE_KEY);

    if (valeurStockee === null) {
      return [];
    }

    const donnees = JSON.parse(valeurStockee);
    let elements;

    if (Array.isArray(donnees)) {
      elements = donnees;
    } else if (
      donnees &&
      typeof donnees === "object" &&
      donnees.version === STORAGE_VERSION &&
      Array.isArray(donnees.taches)
    ) {
      elements = donnees.taches;
    } else {
      return [];
    }

    return elements.filter(validerTache);
  } catch (erreur) {
    console.error("Impossible de restaurer les tâches :", erreur);
    return [];
  }
}

async function chargerTachesDistantes() {
  const reponse = await fetch(API_URL);

  if (!reponse.ok) {
    throw new Error(`Réponse HTTP ${reponse.status}`);
  }

  const donnees = await reponse.json();

  return donnees.map(function (element) {
    return {
      id: `distant-${element.id}`,
      titre: element.title,
      terminee: element.completed
    };
  });
}

async function gererChargement() {
  boutonCharger.disabled = true;
  message.textContent = "Chargement des tâches…";

  try {
    const tachesDistantes = await chargerTachesDistantes();
    message.textContent = `${tachesDistantes.length} tâches reçues. Elles ne sont pas encore ajoutées à votre liste.`;
  } catch (erreur) {
    console.error("Échec du chargement des tâches :", erreur);
    message.textContent = "Le chargement a échoué. Vérifiez votre connexion et réessayez.";
  } finally {
    boutonCharger.disabled = false;
  }
}

boutonCharger.addEventListener("click", gererChargement);
Aperçu du rendu
Chargement de l'aperçu…

Dans try, le code attend le résultat du chargement. Si une erreur survient, catch la journalise et présente un message compréhensible, sans exposer les détails techniques à l’utilisateur. finally s’exécute dans les deux cas : le bouton redevient donc utilisable après une réussite comme après un échec.

3. Vérifier le comportement

Ouvrez la page depuis un serveur local, puis cliquez sur « Charger des tâches ». Pendant la requête, vous voyez le message de chargement et le bouton est désactivé ; si le serveur répond, le nombre d’éléments reçus apparaît, mais la liste des tâches reste inchangée.

Pour observer l’échec réseau, désactivez temporairement la connexion avant de cliquer. Le message d’erreur s’affiche et le bouton est réactivé ; une réponse HTTP non réussie suit le même chemin grâce à l’erreur lancée dans chargerTachesDistantes.

À retenir

  • Utilisez response.ok pour transformer une réponse HTTP non réussie en erreur traitable.
  • Placez l’attente de la requête dans try et affichez un message adapté dans catch.
  • Réservez finally aux actions indispensables dans tous les cas, comme réactiver un bouton.
  • Annoncez le chargement et son résultat dans un élément de statut accessible.
  • À ce stade, les tâches reçues ne remplacent pas encore l’état local ni le contenu de la liste.

Dans la prochaine leçon, vous assemblerez le rendu et les mises à jour pour intégrer les interactions du gestionnaire autour d’un état cohérent.

Envie d'aller plus loin ?

Les cours Enovatic sont gratuits et le resteront. Si vous cherchez un accompagnement encadré, nos formations professionnelles sont animées par les mêmes formateurs.