À propos de ce cours
Maîtrisez l'API fetch pour récupérer des données : syntaxe avec then et async/await, objet Response (ok, status, headers, json, text, blob), piège des erreurs HTTP non rejetées, fonction getJson réutilisable, tableau de toutes les erreurs, URLSearchParams, délai maximal et annulation avec AbortSignal et AbortController, requêtes en série et en parallèle, exemple complet d'annuaire. Avec des activités corrigées.
-
Objectifs
- Récupérer des données avec
fetch, avecthen()puis avecasync/await. - Exploiter l'objet
Response:ok,status, en-têtes,json(),text(),blob(). - Gérer toutes les erreurs : erreurs HTTP, réseau, délai dépassé, JSON invalide.
- Construire des adresses avec
URLSearchParams, lancer des requêtes en parallèle et annuler une requête. - Afficher les données dans la page, avec un état de chargement.
- Récupérer des données avec
-
La fonction fetch
const promesse = fetch(url, options);
url: l'adresse de la ressource, absolue (https://…) ou relative à la page (stagiaires.json,/api/stagiaires).options(facultatif) : un objet qui précise la méthode, les en-têtes, le corps… Sans options,fetchenvoie une requête GET.fetchrenvoie immédiatement une promesse (chapitre 18) ; elle sera tenue avec un objetResponsedès que les en-têtes de la réponse seront arrivés.- Lire le corps de la réponse est une deuxième opération asynchrone :
reponse.json()renvoie une autre promesse, tenue avec les données converties.

Premier exemple : then() ou async/await
// Version avec then()
fetch("stagiaires.json")
.then((reponse) => reponse.json())
.then((stagiaires) => console.log(stagiaires))
.catch((erreur) => console.error("Échec :", erreur));
// Même chose avec async / await
async function chargerStagiaires() {
const reponse = await fetch("stagiaires.json");
const stagiaires = await reponse.json();
console.log(stagiaires);
}
chargerStagiaires();
- Les deux écritures font exactement la même chose. Dans la suite du cours, on utilise
async/await, plus lisible dès qu'il y a plusieurs étapes. - Rappel :
awaitn'est permis que dans une fonctionasync, ou au premier niveau d'un module (<script type="module">). - Comme pour XMLHttpRequest, testez avec Live Server et non en ouvrant le fichier directement.
L'objet Response
const reponse = await fetch("stagiaires.json");
console.log(reponse.ok); // true si le statut est entre 200 et 299
console.log(reponse.status); // 200
console.log(reponse.statusText); // "OK"
console.log(reponse.url); // adresse complète finale (après redirections)
console.log(reponse.headers.get("Content-Type")); // "application/json"
const donnees = await reponse.json(); // le corps ne peut être lu qu'UNE fois
| Méthode | Renvoie (dans une promesse) | Pour… |
|---|---|---|
json() | Un objet ou un tableau JavaScript | Les API (cas le plus courant) |
text() | Une chaîne | Texte, HTML, CSV, Markdown |
blob() | Un Blob (fichier binaire) | Images, PDF, téléchargements (URL.createObjectURL(blob)) |
formData() | Un objet FormData | Réponses de formulaires (rare) |
arrayBuffer() | Des octets bruts | Traitements binaires (audio, fichiers) |
- Le corps est un flux: il ne peut être lu qu'une seule fois. Appeler
json()puistext()sur la même réponse provoque l'erreur « body stream already read ». Si vous avez besoin des deux, utilisezreponse.clone(). reponse.headersest un objetHeaders:get(nom)(insensible à la casse),has(nom), et on peut le parcourir avecfor…of. Par sécurité, une page ne voit que certains en-têtes des réponses venant d'un autre site.
Le piège n° 1 : fetch ne rejette pas sur un 404
- La promesse de
fetchest tenue dès que le serveur a répondu, quel que soit le code: 200, mais aussi 404 ou 500. Elle n'est rejetée que si aucune réponse n'a pu être obtenue : réseau coupé, nom de domaine inexistant, refus CORS, annulation. - Il faut donc toujours tester
reponse.ok(vrai pour les statuts 200 à 299). Sinon,reponse.json()essaiera de lire une page d'erreur HTML et lèvera uneSyntaxErrorincompréhensible pour l'utilisateur. - On écrit une fois pour toutes une petite fonction réutilisable :
// Fonction réutilisable : GET + vérification du statut + JSON
async function getJson(url, options = {}) {
const reponse = await fetch(url, options);
if (!reponse.ok) {
throw new Error(`Erreur HTTP ${reponse.status} pour ${url}`);
}
return reponse.json();
}
try {
const stagiaires = await getJson("stagiaires.json");
console.log(stagiaires.length, "stagiaires");
await getJson("inexistant.json"); // lève « Erreur HTTP 404 »
} catch (erreur) {
console.error(erreur.message);
}
-
Toutes les erreurs possibles
| Situation | Ce qui se passe | Comment la reconnaître |
|---|---|---|
| Réponse 4xx ou 5xx | Promesse tenue | !reponse.ok, reponse.status |
| Pas de réseau, serveur injoignable, refus CORS | Promesse rejetée | TypeError (« Failed to fetch ») |
Délai dépassé (AbortSignal.timeout()) | Promesse rejetée | erreur.name === "TimeoutError" |
Annulation volontaire (AbortController) | Promesse rejetée | erreur.name === "AbortError" |
| Corps qui n'est pas du JSON valide | Promesse de json() rejetée | SyntaxError |
- Pour des raisons de sécurité, le navigateur ne dit pas à JavaScript pourquoi une requête a échoué (réseau ou CORS) : le détail n'apparaît que dans la console.
- Montrez toujours à l'utilisateur un message compréhensible (« Impossible de joindre le service, vérifiez votre connexion ») et gardez le détail technique pour la console.
Construire l'adresse : URLSearchParams
- Les paramètres d'une requête GET se placent dans l'URL. Ne les assemblez pas à la main : un espace, un accent ou un
&dans une valeur casserait l'adresse.URLSearchParamsencode tout correctement :
const params = new URLSearchParams({
q: "développeur web & mobile",
ville: "Gabès",
page: 2,
});
params.append("tri", "date"); // ajouter un paramètre
const url = `https://api.emploi.tn/offres?${params}`;
// https://api.emploi.tn/offres?q=d%C3%A9veloppeur+web+%26+mobile&ville=Gab%C3%A8s&page=2&tri=date
const offres = await getJson(url);
- Pour encoder une seule valeur,
encodeURIComponent("Gabès")donne"Gab%C3%A8s".
Délai maximal et annulation
- Sans précaution, une requête peut rester en attente très longtemps sur un réseau mobile faible. Et une requête lancée peut devenir inutile, par exemple quand l'utilisateur tape une nouvelle lettre dans un champ de recherche : sa réponse, si elle arrive en retard, pourrait même écraser la bonne.
- L'option
signalpermet d'abandonner une requête :
// 1. Abandonner automatiquement au bout de 5 secondes
try {
const reponse = await fetch("https://api.exemple.tn/lent", {
signal: AbortSignal.timeout(5000),
});
console.log(await reponse.json());
} catch (erreur) {
if (erreur.name === "TimeoutError") {
console.error("Trop lent : requête abandonnée.");
}
}
// 2. Annuler soi-même une requête devenue inutile
const controleur = new AbortController();
fetch("https://api.exemple.tn/recherche?q=tun", { signal: controleur.signal }).catch((erreur) => {
if (erreur.name === "AbortError") {
console.log("Requête annulée");
}
});
controleur.abort(); // par exemple parce que l'utilisateur a tapé une autre lettre
AbortSignal.timeout(ms)crée un signal qui se déclenche tout seul après le délai.AbortControllercrée un signal que l'on déclenche soi-même avecabort().AbortSignal.any([signal1, signal2])combine les deux : la requête s'arrête au premier des deux événements. Ces trois outils fonctionnent dans tous les navigateurs récents ; le TP 19 les utilise.
Plusieurs requêtes : en série ou en parallèle
// En SÉRIE : la 2e attend la 1re (utile si elle en dépend)
const utilisateur = await getJson(`${API}/users/1`);
const articles = await getJson(`${API}/posts?userId=${utilisateur.id}`);
// En PARALLÈLE : les deux partent en même temps → deux fois plus rapide
const [articles2, taches] = await Promise.all([
getJson(`${API}/users/2/posts`),
getJson(`${API}/users/2/todos`),
]);
// Promise.allSettled : attend tout, même si certaines échouent (leçon 6)
Promise.alléchoue dès qu'une des requêtes échoue. UtilisezPromise.allSettledquand chaque résultat est utile même si les autres ont échoué.
Exemple complet : un annuaire
Cet exemple interroge l'API de démonstration gratuite JSONPlaceholder (de faux utilisateurs, articles et tâches, avec CORS autorisé). Il charge la liste des utilisateurs, permet de la filtrer, et affiche le détail d'un utilisateur en lançant deux requêtes en parallèle. Si l'on clique sur un autre utilisateur avant la fin, le chargement précédent est annulé.
index.html:
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<title>Annuaire – fetch GET</title>
<link rel="stylesheet" href="style.css" />
<script type="module" src="annuaire.js"></script>
</head>
<body>
<h1>Annuaire des utilisateurs</h1>
<input id="recherche" type="search" placeholder="Filtrer par nom ou ville…" />
<p id="etat">Chargement…</p>
<div class="colonnes">
<ul id="utilisateurs"></ul>
<section id="detail"></section>
</div>
</body>
</html>
annuaire.js:
const API = "https://jsonplaceholder.typicode.com";
const etat = document.getElementById("etat");
const listeUtilisateurs = document.getElementById("utilisateurs");
const detail = document.getElementById("detail");
let utilisateurs = [];
let controleur = null;
async function getJson(url, signal) {
const reponse = await fetch(url, { signal });
if (!reponse.ok) {
throw new Error(`Erreur HTTP ${reponse.status}`);
}
return reponse.json();
}
function creer(balise, texte) {
const element = document.createElement(balise);
element.textContent = texte;
return element;
}
function afficherListe(filtre = "") {
const mot = filtre.trim().toLowerCase();
const visibles = utilisateurs.filter(
(utilisateur) =>
utilisateur.name.toLowerCase().includes(mot) ||
utilisateur.address.city.toLowerCase().includes(mot),
);
listeUtilisateurs.replaceChildren(
...visibles.map((utilisateur) => {
const li = creer("li", `${utilisateur.name} – ${utilisateur.address.city}`);
li.dataset.id = utilisateur.id;
return li;
}),
);
etat.textContent = `${visibles.length} utilisateur(s) sur ${utilisateurs.length}`;
}
// Détail : deux requêtes lancées EN PARALLÈLE avec Promise.all
async function afficherDetail(id) {
controleur?.abort(); // on annule le chargement du détail précédent
controleur = new AbortController();
detail.replaceChildren(creer("p", "Chargement du détail…"));
try {
const [articles, taches] = await Promise.all([
getJson(`${API}/users/${id}/posts`, controleur.signal),
getJson(`${API}/users/${id}/todos?completed=false`, controleur.signal),
]);
const utilisateur = utilisateurs.find((candidat) => candidat.id === id);
const titres = creer("ol", "");
titres.append(...articles.slice(0, 5).map((article) => creer("li", article.title)));
detail.replaceChildren(
creer("h2", utilisateur.name),
creer("p", `✉️ ${utilisateur.email} · 🏢 ${utilisateur.company.name}`),
creer("h3", `Derniers articles (${articles.length} au total)`),
titres,
creer("p", `📝 ${taches.length} tâche(s) non terminée(s)`),
);
} catch (erreur) {
if (erreur.name !== "AbortError") {
detail.replaceChildren(creer("p", `Impossible de charger le détail : ${erreur.message}`));
}
}
}
listeUtilisateurs.addEventListener("click", (event) => {
const li = event.target.closest("li");
if (li) {
listeUtilisateurs.querySelector(".actif")?.classList.remove("actif");
li.classList.add("actif");
afficherDetail(Number(li.dataset.id));
}
});
document.getElementById("recherche").addEventListener("input", (event) => {
afficherListe(event.target.value);
});
try {
utilisateurs = await getJson(`${API}/users`);
afficherListe();
} catch (erreur) {
etat.textContent = `Chargement impossible : ${erreur.message}`;
}
- La liste complète est chargée une seule fois; le filtre travaille ensuite sur la variable
utilisateurs, sans nouvelle requête. - Toutes les données sont insérées avec
textContent(fonctioncreer()) : même si l'API renvoyait du code HTML malveillant, il serait affiché comme du texte (protection contre les failles XSS). - Un message de chargement est affiché pendant l'attente, puis remplacé par le résultat ou par l'erreur.
Bonnes pratiques
- États de l'interface : prévoyez toujours trois affichages : chargement (indicateur, bouton désactivé), succès, erreur (message et, si possible, bouton « Réessayer »). Pensez aussi au cas « aucun résultat ».
- Ne bloquez pas inutilement : lancez en parallèle les requêtes indépendantes.
- N'insérez jamais de données reçues avec
innerHTML. - Centralisez les appels dans un module (
api.js) : l'adresse de l'API et la gestion des erreurs sont écrites une seule fois. - Le cache HTTP fonctionne aussi avec
fetch: une réponse avec un en-têteCache-Controlpeut être réutilisée sans nouvel appel. L'optioncache: "no-store"force une nouvelle requête.
Activités
Activité 1 : premiers pas
- Dans le dossier
fetch-getde votre dépôtjs-ajax-fetch, chargezstagiaires.jsonavecfetchet affichez chaque stagiaire dans un tableau HTML (nom, groupe, moyenne). - Affichez dans la console
reponse.status,reponse.oket l'en-têteContent-Type. Puis demandezstagiaire.json(sans « s ») : que valentoketstatus? Que se passe-t-il si vous appelez quand mêmereponse.json()?
Activité 2 : l'annuaire
- Reproduisez l'exemple complet. Avec DevTools (Réseau → Slow 3G), cliquez rapidement sur trois utilisateurs : combien de requêtes sont annulées (statut canceled) ?
- Ajoutez un bouton « Réessayer » quand le chargement de la liste échoue (testez avec le mode Offline).
- Ajoutez dans le détail la liste des albums de l'utilisateur (
/users/{id}/albums), chargée en parallèle avec les deux autres requêtes.
Activité 3 : une image
- Avec
fetchetblob(), téléchargez une image de votre site et affichez-la dans un<img>grâce àURL.createObjectURL(). Affichez aussi sa taille en Ko (blob.size) et son type (blob.type).
Activité 4 : questions
- La promesse de
fetchest-elle rejetée pour une réponse 500 ? Que faut-il tester ? - Pourquoi ne peut-on pas appeler deux fois
reponse.json()? - Deux requêtes indépendantes prennent chacune 800 ms. Combien de temps faut-il environ en série ? Et avec
Promise.all? - À quoi sert
URLSearchParams?
Corrigés. Activité 1 : pour
stagiaire.json, ok vaut false et status 404 ; reponse.json() lève une SyntaxError car le corps est une page d'erreur et non du JSON. Activité 2 : chaque clic annule la requête précédente ; seules les requêtes du dernier utilisateur arrivent à leur terme. Activité 3 : const blob = await (await fetch("logo.png")).blob(); img.src = URL.createObjectURL(blob);. Activité 4 : non, elle est tenue ; il faut tester reponse.ok ou reponse.status; le corps est un flux qui ne se lit qu'une fois ; environ 1,6 s en série et 0,8 s en parallèle ; à construire des paramètres d'URL correctement encodés.À retenir
const reponse = await fetch(url)puisconst donnees = await reponse.json().- Testez toujours
reponse.ok: un 404 ou un 500 ne rejette pas la promesse. URLSearchParamspour les paramètres,AbortSignal.timeout()etAbortControllerpour le délai et l'annulation,Promise.allpour le parallèle.- Affichez un état de chargement, des erreurs compréhensibles, et insérez les données avec
textContent.
Sources : MDN Web Docs, WHATWG (XMLHttpRequest, Fetch), IETF, open-meteo.com.