Question 1Quand tu fais await fetch(url) avec une URL dont le domaine n'existe pas, quel error.name arrive dans le catch ?
Gestion des erreurs de fetch et timeouts — AbortController et nouvel essai
Les erreurs HTTP, pour lesquelles fetch ne lève aucune exception, les erreurs réseau, où fetch échoue lui-même, le timeout avec AbortController et setTimeout, et un seul nouvel essai.
Lire response.ok te permet de savoir quand le serveur répond qu'un fichier n'existe pas. Mais si la connexion est coupée, la ligne await fetch lève une exception avant même que tu arrives à ok. Et si la réponse est lente, le code reste à attendre au lieu de passer à la ligne suivante.
Cet article montre comment traiter chaque type d'échec de fetch comme une exception, et comment annuler une requête avec AbortController.
Transformer une réponse d'échec en exception — throw new Error
Trois écrans différents chargent la liste des commandes. Si loadJson renvoie null en cas d'échec, comme dans l'article précédent, chaque écran doit vérifier null, et si l'un d'eux oublie, la ligne suivante qui lit la valeur lève une erreur.
En cas d'erreur HTTP (une requête qui a bien atteint le serveur, mais qui reçoit un code de statut d'échec comme 403 ou 404), fetch ne lève pas d'exception. Si tu fais throw new Error(...) quand ok vaut false, l'appelant peut gérer l'échec à un seul endroit, avec try / catch.
// Si ok vaut false, lève une exception qui indique le code de statut
async function loadJson(path) {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${path}`);
}
return await response.json();
}
try {
const orders = await loadJson("fixtures/fr/order.json"); // cette ligne lève une exception
console.log(`Commandes : ${orders.length}`); // jamais exécutée
} catch (error) {
// 403 dans la console des exercices, 404 ou autre sur les autres serveurs
console.log(error.message); // HTTP 403: fixtures/fr/order.json
}
Avec throw, même si un écran oublie le catch, l'erreur non interceptée indique toujours HTTP 403. Au lieu d'écrire sur chaque écran un if qui vérifie null, il te suffit de choisir l'endroit où intercepter l'échec.
Distinguer les échecs réseau — TypeError et instanceof
Quand tu perds le réseau en déplacement, la requête n'atteint jamais le serveur et aucune réponse ne revient. Cet échec arrive dans le même catch qu'une erreur HTTP : en l'état, tu afficherais donc le même message pour « le fichier n'existe pas » et pour « connexion impossible ».
En cas d'erreur réseau (un échec où aucune réponse n'arrive, par exemple parce que la connexion est coupée ou que le domaine n'existe pas), la Promise renvoyée par fetch passe à rejected avec une TypeError. Si tu testes instanceof TypeError dans le catch, tu peux la distinguer d'une Error que tu as levée toi-même.
// loadJson est la même que dans la section précédente (lève une exception si ok vaut false)
const urls = [
"fixtures/fr/order.json", // atteint le serveur, mais le fichier n'existe pas
"https://shop.invalid/orders.json", // un domaine en .invalid ne mène nulle part
];
for (const url of urls) {
try {
await loadJson(url);
} catch (error) {
// Une Error que tu as levée, ou une TypeError levée par fetch ?
const kind = error instanceof TypeError ? "Connexion impossible" : "Requête échouée";
console.log(`${kind} / ${error.name}: ${error.message}`);
}
}
// Requête échouée / Error: HTTP 403: fixtures/fr/order.json
// Connexion impossible / TypeError: Failed to fetch
- Les deux échecs lèvent une exception sur cette ligne et passent au
catch - Dans le
catch, testeerror instanceof TypeErrorpour les distinguer
if (!response.ok)— pourorder.json, une réponse arrive maisokvautfalsethrow new Error(...)— uneErrorlevée par ton code
https://shop.invalid/…— aucune réponse n'arriveTypeError: Failed to fetch— levée par fetch
Une TypeError venue du fetch intérieur sort de loadJson sans jamais atteindre son if. Comme chaque écran affiche son propre message, loadJson se contente de lever l'exception : c'est le catch de l'appelant, qui reçoit les deux types d'échec, qui teste le type.
Le message d'erreur réseau varie selon le navigateur
Le message de la TypeError levée par fetch change d'un navigateur à l'autre : Failed to fetch dans Chrome, Load failed dans Safari. Si tu compares le texte du message, le code part dans la mauvaise branche sur les autres navigateurs. Teste plutôt le type avec instanceof TypeError.
Interrompre une requête trop longue — AbortController
Un serveur surchargé peut mettre des dizaines de secondes à répondre. fetch n'a aucune option pour fixer un temps d'attente maximal : la ligne await fetch n'avance pas tant qu'aucune réponse n'est arrivée, et l'écran semble bloqué sur le chargement. Dans cette section, le code utilise slowFetch, un substitut de fetch qui met 300 ms à répondre.
Si tu crées un AbortController (un objet qui permet d'annuler un fetch en cours) et que tu le passes sous la forme fetch(url, { signal: controller.signal }), appeler abort() fait échouer la requête avec une AbortError.
Pour annuler un appel programmé, passe à clearTimeout l'identifiant de minuteur (un nombre qui désigne l'appel programmé) renvoyé par setTimeout.
// slowFetch est un substitut de fetch défini dans la console de l'exercice 3 ; il met 300 ms à répondre et s'utilise comme fetch
const controller = new AbortController();
// Programme abort() dans 100 ms et garde l'identifiant du minuteur
const timerId = setTimeout(() => controller.abort(), 100);
try {
// Comme avec fetch, passe signal en deuxième argument
const response = await slowFetch("fixtures/fr/orders.json", { signal: controller.signal });
console.log(response.ok); // la réponse met 300 ms, donc cette ligne ne s'exécute jamais
} catch (error) {
console.log(error.name); // AbortError
} finally {
clearTimeout(timerId); // annule l'appel programmé, au cas où la réponse arriverait avant
}
Placer clearTimeout dans finally garantit qu'aucun appel programmé ne reste en attente, que la requête réussisse ou échoue. Si abort() s'exécute avant que le corps ait été lu, json() échoue lui aussi avec une AbortError : garde donc return await response.json() dans le try, pour ne passer au finally qu'une fois le corps lu.
Réessayer une seule fois — nouvel essai et return await
Sur un téléphone, dans un train en marche, la connexion peut se couper un instant et faire échouer une requête, alors qu'un nouvel essai immédiat réussit souvent. Plutôt que de demander à l'utilisateur de recharger à chaque fois, ton code peut réessayer automatiquement et garder les données à l'écran.
Pour faire un nouvel essai (renvoyer une requête échouée vers la même URL), appelle à nouveau la fonction dans le catch. Sans limite sur le nombre d'essais, le code enverrait des requêtes tant que la connexion reste coupée : si le deuxième essai échoue lui aussi, l'erreur remonte donc directement à l'appelant.
// loadJson est la même que dans la première section. Réessaie une seule fois, et uniquement en cas d'échec réseau ou de timeout
async function loadWithRetry(path) {
try {
return await loadJson(path);
} catch (error) {
const retryable = error instanceof TypeError || error.name === "AbortError";
if (!retryable) {
throw error; // renvoyer la requête donnerait le même 403 ou 404
}
console.log(`Nouvel essai après ${error.name}`); // Nouvel essai après TypeError
return await loadJson(path); // un deuxième échec remonte directement à l'appelant
}
}
try {
await loadWithRetry("https://shop.invalid/orders.json");
} catch (error) {
console.log(`Échec des deux essais : ${error.name}`); // Échec des deux essais : TypeError
}
Une erreur HTTP, comme celle de fixtures/fr/order.json, est relancée dans le premier catch : elle remonte donc à l'appelant sans deuxième appel. Le tableau ci-dessous indique comment repérer chaque type d'échec dans le catch, et s'il faut réessayer.
| Échec | Test dans le catch | Réessayer ? |
|---|---|---|
| Erreur réseau | error instanceof TypeError | Une fois (réseau rétabli) |
| Timeout (abort()) | error.name === "AbortError" | Une fois (charge réduite) |
| Erreur HTTP (403, 404) | Tous les autres cas | Non (même réponse) |
return sans await ne passe pas par catch
Si tu écris try { return loadJson(path); } sans await, la fonction quitte le try dès qu'elle renvoie la Promise. Même si cette Promise passe ensuite à rejected, le catch ne s'exécute pas, et l'échec remonte directement à l'appelant. Écris plutôt return await loadJson(path);.
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Tu programmes abort() dans 100 ms et tu passes signal à une requête qui met 300 ms à répondre. Que se passe-t-il ?
Question 3Quel échec le loadWithRetry de cet article relance-t-il sans réessayer ?