Apprenez en lisant dans l'ordre

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, la cause arrive jusqu'au catch
Si ok vaut false,renvoie nullordersvaut nullLecture deorders.lengthTypeError, etle 403 est perduSi ok vaut false,throwawait loadJsonlève l'exceptionSaute la suite,passe au catcherror.message vautHTTP 403: …
Si tu renvoies null, c'est une autre erreur qui survient sur la ligne orders.length. Avec throw, un message qui contient le code de statut arrive jusqu'au catch.

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.

Sur un écran de suivi des livraisons, tu charges la liste des commandes. Le squelette de la fonction loadOrders est déjà en place, et les deux chemins figurent en haut de la console.

① Si la réponse signale un échec, lève une exception dont le message est « Impossible de charger les commandes : » suivi du chemin.

② En cas de succès, transforme le corps en tableau et renvoie-le.

③ Charge les commandes avec le bon chemin et affiche leur montant total.

④ Charge-les avec le chemin mal écrit et, en cas d'échec, affiche le message de l'exception.

(Si tout s'exécute correctement, une explication apparaîtra.)

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
D'où viennent les deux types d'échec
Le try de l'appelant — ligne await loadJson(url)
  • Les deux échecs lèvent une exception sur cette ligne et passent au catch
  • Dans le catch, teste error instanceof TypeError pour les distinguer
Dans loadJson
  • if (!response.ok) — pour order.json, une réponse arrive mais ok vaut false
  • throw new Error(...) — une Error levée par ton code
Dans await fetch(url)
  • https://shop.invalid/… — aucune réponse n'arrive
  • TypeError: Failed to fetchlevée par fetch
Les erreurs HTTP sont levées par loadJson, les erreurs réseau par fetch. Les deux arrivent dans le même catch : distingue-les par leur type.

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.

Sur un écran de gestion des stocks, tu affiches un message différent selon le type d'échec. loadJson et urls (trois URL) sont déjà déclarés.

① Dans showStock, charge la liste des produits et affiche une ligne comme « Produits : 4 ».

② Si la requête n'a pas atteint le serveur, affiche « Impossible de joindre le serveur ».

③ Pour tout autre échec, affiche « Impossible de charger la liste des produits ».

④ Passe tour à tour chaque URL de urls à showStock.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
}
Qui arrive en premier : le minuteur ou la réponse ?
Délai 100 msréponse à 300 msÀ 100 ms,abort() s'exécutefetch échoueavec AbortErrorcatch signalele délai dépasséDélai 3000 msréponse à 300 msÀ 300 ms, laréponse arriveclearTimeoutdans finallyabort() n'estjamais appelé
Si la réponse est plus lente que le délai, abort() s'exécute d'abord et la requête échoue. Si la réponse arrive à temps, annule le abort() programmé.

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.

Sur l'écran de détail d'un abonnement, tu fixes une limite de temps au chargement de la liste des membres. slowFetch, qui simule une API lente, est déjà déclaré.

① Dans loadUsers, programme l'annulation de la requête une fois le délai écoulé.

② Récupère la liste des membres avec slowFetch de façon à pouvoir l'annuler, et renvoie le tableau du corps.

③ Annule l'appel programmé, que la requête réussisse ou échoue.

④ Avec un délai de 3000 ms, affiche le nombre de membres ; avec un délai de 100 ms, affiche le nom de l'erreur.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
}
Un seul nouvel essai, pas plus
loadWithRetry(path) appelée1er essairéussi1er essaiTypeError1er essaiTypeErrorPas de2e appel2e essairéussi2e essai aussiTypeErrorRenvoie le tableaudu 1er essaiMessage affiché,tableau renvoyéPas de 3e essai :lève l'exception
Si le premier essai réussit, il n'y a pas de deuxième appel. Si le deuxième essai échoue aussi, il n'y en a pas de troisième : l'exception remonte à l'appelant.

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.

ÉchecTest dans le catchRéessayer ?
Erreur réseauerror instanceof TypeErrorUne fois (réseau rétabli)
Timeout (abort())error.name === "AbortError"Une fois (charge réduite)
Erreur HTTP (403, 404)Tous les autres casNon (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);.

Tu charges la liste des membres depuis une API lente. slowFetch et loadWithin sont déjà déclarés.

① Dans loadWithRetry, charge avec un délai de limitMs et renvoie le résultat.

② Uniquement en cas de timeout, affiche « Délai dépassé, nouvel essai » et recharge avec un délai de 3000 ms.

③ Charge les membres avec loadWithRetry et affiche une ligne comme « Membres : 4 ».

④ Charge aussi le chemin erroné, et affiche « Abandon : » suivi du message de l'exception.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Quand tu fais await fetch(url) avec une URL dont le domaine n'existe pas, quel error.name arrive dans le catch ?

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 ?