Consommer une API avec fetch en JavaScript et gérer correctement les erreurs

Une promesse fetch résolue ne signifie pas toujours que la requête a réussi. Construisez une fonction robuste qui distingue erreur réseau, réponse HTTP, JSON invalide et données inattendues.

Votre objectif est d’appeler une API depuis une page web et d’afficher son résultat sans masquer les pannes ni exposer d’information sensible. Ce tutoriel est de niveau débutant à intermédiaire. Il utilise JavaScript moderne dans un navigateur compatible avec fetch, async/await, AbortController et les modules JavaScript.

Une API, ou interface de programmation, permet à deux logiciels d’échanger des requêtes et des données. La fonction fetch envoie une requête HTTP depuis JavaScript. Point essentiel : elle rejette sa promesse lors de certaines erreurs réseau, mais une réponse HTTP 404 ou 500 doit être contrôlée explicitement.

Préparer un environnement de test sûr

Utilisez Visual Studio Code ou un autre éditeur, puis servez les fichiers avec un petit serveur local. Ouvrir directement un fichier avec une adresse file:// peut produire un comportement différent en raison des règles de sécurité du navigateur.

Créez deux fichiers :

  • index.html pour l’interface ;
  • app.js pour le code JavaScript.

Employez une API de démonstration ou votre propre environnement de test. N’insérez jamais une clé secrète dans du JavaScript envoyé au navigateur : tout visiteur peut consulter le code et les requêtes. Les secrets doivent rester sur un serveur contrôlé.

<!-- index.html -->
<button id="load" type="button">Charger le profil</button>
<p id="status" role="status" aria-live="polite"></p>
<section id="result" aria-labelledby="result-title" hidden>
  <h2 id="result-title">Profil reçu</h2>
  <p id="name"></p>
</section>
<script type="module" src="app.js"></script>

L’attribut aria-live demande aux technologies d’assistance d’annoncer les changements de statut sans déplacer automatiquement le focus. L’attribut hidden masque la section tant qu’aucune donnée valide n’est disponible.

Écrire un premier appel avec async et await

Une promesse représente une opération qui produira un résultat ou une erreur plus tard. Le mot-clé await suspend la fonction asynchrone jusqu’au règlement de cette promesse, sans bloquer toute l’interface du navigateur.

async function chargerProfil() {
  const response = await fetch('https://api.exemple.test/users/42');
  const data = await response.json();
  return data;
}

Cette version est lisible, mais incomplète. Elle suppose que le serveur répond avec succès, que le corps contient du JSON valide et que la structure correspond aux attentes. JSON est un format texte utilisé pour représenter des objets et tableaux de données.

Vérifier le statut HTTP avant de lire les données

Requêtes fetch avec différents codes de statut dans l’onglet Réseau

Une réponse HTTP contient un code de statut. Les codes 200 à 299 indiquent généralement un succès ; 400 à 499 signalent un problème lié à la requête ou à l’autorisation ; 500 à 599 décrivent une erreur côté serveur. La propriété response.ok vaut true pour les statuts compris dans la plage de succès.

class HttpError extends Error {
  constructor(message, status) {
    super(message);
    this.name = 'HttpError';
    this.status = status;
  }
}

async function demanderJson(url, options = {}) {
  const response = await fetch(url, options);

  if (!response.ok) {
    throw new HttpError(
      `La requête a échoué avec le statut ${response.status}`,
      response.status
    );
  }

  return response.json();
}

Cette erreur personnalisée conserve le statut sans afficher au public le détail technique du serveur. En production, évitez de présenter directement le corps d’une réponse d’erreur : il peut contenir des informations internes ou un format imprévu.

Distinguer un JSON invalide d’une réponse HTTP en échec

Même avec un statut de succès, response.json() peut échouer si le corps est vide ou mal formé. Ajoutez une erreur dédiée pour faciliter le diagnostic :

async function demanderJson(url, options = {}) {
  const response = await fetch(url, options);

  if (!response.ok) {
    throw new HttpError('Réponse HTTP en échec', response.status);
  }

  try {
    return await response.json();
  } catch {
    throw new Error('La réponse ne contient pas un JSON valide');
  }
}

Si votre API peut répondre sans contenu, par exemple avec le statut 204, gérez ce cas avant d’appeler json(). Le contrat de l’API doit préciser les statuts et formats attendus.

Valider la structure avant d’utiliser la réponse

Recevoir du JSON ne garantit pas la présence des propriétés attendues. Une API peut évoluer, renvoyer null ou fournir un type différent. Validez au minimum les champs utilisés par l’interface.

function estProfilValide(value) {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof value.name === 'string' &&
    value.name.trim().length > 0
  );
}

Cette validation manuelle convient à un petit objet. Pour des structures complexes, une bibliothèque de validation de schéma peut être envisagée, mais elle ajoute une dépendance, un poids et une version à maintenir. Dans tous les cas, validez également les données côté serveur lorsqu’elles influencent une opération sensible.

Ajouter un délai maximal avec AbortController

Une requête peut rester en attente longtemps. AbortController fournit un signal permettant de l’annuler. Le code suivant applique un délai maximal configurable et nettoie toujours le minuteur :

async function demanderJsonAvecDelai(url, { timeout = 8000, ...options } = {}) {
  const controller = new AbortController();
  const timer = window.setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });

    if (!response.ok) {
      throw new HttpError('Réponse HTTP en échec', response.status);
    }

    if (response.status === 204) {
      return null;
    }

    try {
      return await response.json();
    } catch {
      throw new Error('JSON_INVALID');
    }
  } finally {
    window.clearTimeout(timer);
  }
}

Un délai de huit secondes est seulement un exemple de configuration, pas une valeur universelle. Adaptez-le au service, à la qualité réseau attendue et au besoin utilisateur. Une annulation peut également être déclenchée lorsque l’utilisateur quitte une vue ou lance une nouvelle recherche.

Afficher des états accessibles dans l’interface

Interface web montrant les états de chargement, succès et erreur

L’utilisateur doit savoir si la requête charge, réussit ou échoue. Désactivez temporairement le bouton pour limiter les doubles appels, mais réactivez-le dans un bloc finally, exécuté après le succès comme après l’erreur.

const button = document.querySelector('#load');
const status = document.querySelector('#status');
const result = document.querySelector('#result');
const name = document.querySelector('#name');

button.addEventListener('click', async () => {
  button.disabled = true;
  result.hidden = true;
  status.textContent = 'Chargement en cours…';

  try {
    const profile = await demanderJsonAvecDelai(
      'https://api.exemple.test/users/42'
    );

    if (!estProfilValide(profile)) {
      throw new Error('DATA_INVALID');
    }

    name.textContent = profile.name;
    result.hidden = false;
    status.textContent = 'Profil chargé.';
  } catch (error) {
    console.error('Échec du chargement :', error);

    if (error.name === 'AbortError') {
      status.textContent = 'Le service met trop de temps à répondre.';
    } else if (error instanceof HttpError && error.status === 404) {
      status.textContent = 'Ce profil est introuvable.';
    } else {
      status.textContent = 'Impossible de charger le profil pour le moment.';
    }
  } finally {
    button.disabled = false;
  }
});

textContent insère du texte sans l’interpréter comme du HTML. C’est un choix plus sûr que innerHTML pour afficher une valeur reçue d’une API. Si du HTML distant doit réellement être rendu, il faut une stratégie de nettoyage adaptée ; ne considérez jamais une réponse externe comme fiable par défaut.

Comprendre CORS et les limites côté navigateur

CORS, pour Cross-Origin Resource Sharing, est un mécanisme par lequel un serveur indique quelles origines web peuvent lire ses réponses. Si l’API n’autorise pas votre domaine, le navigateur bloque l’accès à la réponse. Ajouter arbitrairement un en-tête dans fetch ne corrige pas ce problème : l’autorisation doit être configurée par le serveur de l’API.

N’utilisez pas un proxy public inconnu pour contourner CORS, surtout avec des données ou jetons sensibles. Si vous contrôlez l’API, configurez une liste d’origines autorisées adaptée. Sinon, passez par votre propre serveur lorsque les conditions d’utilisation du service le permettent.

L’authentification demande aussi une conception spécifique. Un jeton public limité peut parfois être prévu par le fournisseur, mais une clé secrète ne doit pas être intégrée au code client. Utilisez HTTPS afin de protéger le transport des données et appliquez des autorisations minimales côté serveur.

Décider quand relancer une requête

Une nouvelle tentative peut être utile après une panne temporaire ou certains statuts serveur, mais elle n’a aucun intérêt pour une URL inexistante ou une demande non autorisée. Limitez le nombre de tentatives et espacez-les progressivement afin de ne pas surcharger le service.

Les opérations qui modifient des données exigent une précaution supplémentaire. Répéter automatiquement une création ou un paiement peut produire des doublons si le serveur a traité la première requête sans que la réponse atteigne le navigateur. Consultez le contrat de l’API et utilisez les mécanismes d’idempotence prévus. L’idempotence désigne la capacité à répéter une opération sans multiplier son effet.

Tester les principaux scénarios d’échec

Avant le déploiement, testez au minimum :

  1. une réponse 200 avec les données attendues ;
  2. une réponse 204 si elle est permise ;
  3. une ressource absente avec un statut 404 ;
  4. une erreur serveur ;
  5. un corps JSON mal formé ;
  6. un objet dont un champ requis manque ;
  7. une requête annulée ou trop lente ;
  8. plusieurs clics rapides sur le bouton.

Utilisez les outils réseau du navigateur et, si vous contrôlez le projet, des tests automatisés avec des réponses simulées. Ne provoquez pas volontairement de panne sur un service tiers en production.

Actions prioritaires à retenir

  1. Vérifier response.ok avant de lire le corps.
  2. Distinguer erreur réseau, statut HTTP, JSON invalide et données invalides.
  3. Annuler les requêtes trop longues avec un délai adapté.
  4. Afficher un état clair sans exposer les détails techniques.
  5. Utiliser textContent pour les données textuelles non fiables.
  6. Garder les clés secrètes sur le serveur et respecter CORS.

Une fonction robuste ne cherche pas à faire disparaître les erreurs. Elle les classe, protège l’interface et fournit un résultat observable à l’utilisateur comme au développeur. Commencez avec cette structure, puis adaptez les statuts, validations et délais au contrat réel de votre API.

FAQ sur fetch et les erreurs d’API

Pourquoi fetch ne passe-t-il pas dans catch avec une erreur 404 ?

Parce que le serveur a bien fourni une réponse HTTP. Vérifiez response.ok et lancez explicitement une erreur lorsque le statut n’indique pas un succès.

Peut-on cacher une clé API dans une variable JavaScript ?

Non. Le code et les requêtes du navigateur restent consultables. Une clé secrète doit être utilisée par un serveur contrôlé.

Faut-il toujours relancer une requête en échec ?

Non. Relancez seulement les erreurs potentiellement temporaires et lorsque l’opération peut être répétée sans effet indésirable.

Une validation dans le navigateur suffit-elle ?

Non. Elle améliore l’interface, mais toute donnée sensible ou opération importante doit aussi être validée et autorisée côté serveur.

À propos de l’auteur

Maya Connect

Maya Connect est la voix éditoriale de Clic Connect. Curieuse, méthodique et attentive aux usages concrets, elle transforme les sujets numériques complexes en parcours compréhensibles et applicables. Elle relie chaque notion à un objectif précis, explicite les prérequis et distingue systématiquement les faits établis, les bonnes pratiques et les pistes à expérimenter.