fetch() est désormais intégré dans tous les navigateurs modernes et Node.js 18+.
Il a remplacé XMLHttpRequest et éliminé le besoin d'Axios dans la plupart des projets.
Mais l'utilisation par défaut que montrent tous les tutoriels — fetch(url).then(r => r.json())
— ignore la gestion des erreurs, n'a pas de délai d'expiration et s'effondre dans tout environnement de production réel.
Ce guide couvre les patterns qui tiennent vraiment la route.
Les bases — GET et POST
fetch() retourne une Promise qui se résout avec un
objet Response.
Une requête GET est simple :
const response = await fetch('https://api.example.com/products');
const products = await response.json();Une requête POST avec un corps JSON nécessite un peu plus de configuration :
const newProduct = {
name: 'Wireless Keyboard',
price: 79.99,
category: 'electronics'
};
const response = await fetch('https://api.example.com/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(newProduct)
});
const created = await response.json();
console.log(`Created product with id: ${created.id}`);Content-Type: application/json
dans les requêtes POST. Sans cela, de nombreux frameworks serveur ne parseront pas le corps, et vous obtiendrez une
erreur 400 Bad Request ou un corps de requête vide sans message d'erreur utile.La vérification d'erreur en deux étapes — response.ok
C'est le pattern le plus important à intérioriser. fetch() ne rejette
sa Promise que sur les erreurs réseau (pas de connexion, échec DNS, blocage CORS). Une réponse 404, 401 ou 500
résout quand même la Promise — avec response.ok défini à
false. Si vous ne vérifiez pas cela, vous passerez silencieusement les réponses d'erreur à
response.json() :
// ❌ Broken — 404 and 500 responses appear to succeed
async function getProduct(id) {
const response = await fetch(`/api/products/${id}`);
return await response.json(); // parses the error body as if it were data
}
// ✅ Correct — check response.ok before parsing
async function getProduct(id) {
const response = await fetch(`/api/products/${id}`);
if (!response.ok) {
// Try to get the error message from the body if it's JSON
const errorBody = await response.json().catch(() => null);
const message = errorBody?.message ?? `HTTP ${response.status}: ${response.statusText}`;
throw new Error(message);
}
return await response.json();
}response.ok est true pour les codes de statut 200–299.
Tout le reste — redirections 301 (si non suivies automatiquement), erreurs 400, erreurs 500 — le définit
à false. Vérifiez-le toujours avant de parser.
AbortController — Délais d'expiration et annulation
fetch() n'a pas de délai d'expiration intégré. Une requête peut se bloquer indéfiniment
si le serveur cesse de répondre en cours de transfert. La solution est
AbortController :
async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (err) {
if (err.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw err;
} finally {
clearTimeout(timeoutId);
}
}
// Usage
try {
const data = await fetchWithTimeout('/api/products', {}, 5000);
} catch (err) {
console.error(err.message); // "Request timed out after 5000ms"
}AbortController est également utile pour annuler des requêtes en cours lorsqu'un utilisateur quitte la page ou effectue une nouvelle recherche avant que la précédente ne soit terminée :
let searchController = null;
async function searchProducts(query) {
// Cancel any previous search request
if (searchController) {
searchController.abort();
}
searchController = new AbortController();
try {
const response = await fetch(
`/api/products/search?q=${encodeURIComponent(query)}`,
{ signal: searchController.signal }
);
return await response.json();
} catch (err) {
if (err.name !== 'AbortError') throw err;
return null; // request was cancelled — that's OK
}
}Nouvelle tentative avec backoff exponentiel
Les requêtes réseau échouent de manière transitoire — un 503 lors d'une nouvelle tentative réussit souvent à la suivante. Le backoff exponentiel est la stratégie standard : attendre progressivement plus longtemps entre les tentatives pour éviter de surcharger un serveur déjà surchargé :
async function fetchWithRetry(url, options = {}, retries = 3, baseDelayMs = 500) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, options);
// Don't retry client errors (4xx) — they won't fix themselves
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error ${response.status} — not retrying`);
}
if (!response.ok) {
throw new Error(`Server error ${response.status}`);
}
return await response.json();
} catch (err) {
const isLastAttempt = attempt === retries;
if (isLastAttempt || err.message.includes('Client error')) {
throw err;
}
// Exponential backoff with jitter: 500ms, 1000ms, 2000ms + random
const delay = baseDelayMs * 2 ** (attempt - 1) + Math.random() * 100;
console.warn(`Attempt ${attempt} failed, retrying in ${Math.round(delay)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const products = await fetchWithRetry('/api/products', {}, 3, 500);Math.random() * 100 au
délai évite le problème du « thundering herd » — où des milliers de clients effectuent tous de nouvelles tentatives exactement au
même moment après un incident serveur. Un petit décalage aléatoire, un grand gain en fiabilité.Un pattern intercepteur — Encapsuler fetch()
Axios a popularisé le concept d'intercepteur : un hook qui s'exécute avant chaque requête
et après chaque réponse. Vous pouvez construire la même chose comme un mince wrapper autour de
fetch() :
// api.js — your project's fetch wrapper
const API_BASE = 'https://api.example.com';
function getAuthToken() {
return localStorage.getItem('authToken');
}
async function apiFetch(path, options = {}) {
const url = `${API_BASE}${path}`;
// Request interceptor — add auth header to every request
const headers = {
'Content-Type': 'application/json',
...options.headers
};
const token = getAuthToken();
if (token) headers['Authorization'] = `Bearer ${token}`;
const response = await fetch(url, { ...options, headers });
// Response interceptor — handle auth expiry globally
if (response.status === 401) {
logout(); // token expired — redirect to login
throw new Error('Session expired');
}
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new Error(body.message ?? `API error ${response.status}`);
}
// Return null for 204 No Content
if (response.status === 204) return null;
return response.json();
}
// Usage — clean, no repeated boilerplate
const products = await apiFetch('/products');
const created = await apiFetch('/products', {
method: 'POST',
body: JSON.stringify({ name: 'New Product', price: 29.99 })
});Ce pattern centralise l'authentification, la gestion des erreurs et la configuration de l'URL de base. Chaque appel API dans votre base de code bénéficie du même comportement automatiquement. Quand les exigences changent — comme ajouter un nouvel en-tête ou passer de JWT aux cookies de session — vous mettez à jour un seul endroit.
Gérer les différents types de réponse
Toutes les API ne retournent pas du JSON. fetch() a des méthodes pour tous les types de réponse courants :
// JSON (most common)
const data = await response.json();
// Plain text (CSV, HTML, logs)
const csvText = await response.text();
// Binary data — download a file
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const downloadLink = document.createElement('a');
downloadLink.href = blobUrl;
downloadLink.download = 'export.csv';
downloadLink.click();
URL.revokeObjectURL(blobUrl); // clean up
// ArrayBuffer — for WebAssembly or typed arrays
const buffer = await response.arrayBuffer();
const byteArray = new Uint8Array(buffer);
// Form data (multipart responses)
const formData = await response.formData();CORS — Ce que les développeurs doivent savoir
CORS (Cross-Origin Resource Sharing) est appliqué par le navigateur, pas par le serveur.
Lorsque votre frontend sur app.example.com effectue un fetch depuis api.example.com,
le navigateur envoie un en-tête Origin et vérifie la réponse pour obtenir la permission.
Voici ce que vous contrôlez côté client :
// credentials: 'include' — send cookies/session tokens cross-origin
// The server must also respond with Access-Control-Allow-Credentials: true
const response = await fetch('https://api.example.com/account', {
credentials: 'include'
});
// credentials: 'same-origin' — default, only sends credentials to same origin
// credentials: 'omit' — never send credentials (useful for public CDN requests)
// mode: 'no-cors' — fire-and-forget for cross-origin requests
// You get a "opaque" response — no status, no body, no error
await fetch('https://analytics.example.com/event', {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify({ event: 'page_view' })
});
// Use for analytics pings where you don't need the responseSi vous obtenez des erreurs CORS, la correction se trouve presque toujours côté serveur : le serveur
doit ajouter Access-Control-Allow-Origin à ses en-têtes de réponse.
Le guide CORS de MDN
est la référence la plus complète pour savoir comment le configurer.
Outils utiles
Lorsque vous déboguez des réponses fetch, JSON Formatter rend les payloads API lisibles et JSON Validator détecte les réponses malformées. Pour encoder les paramètres de requête en toute sécurité, JSON URL Encode est là pour vous. Le guide MDN Using Fetch est la meilleure référence unique pour toutes les options fetch et méthodes de réponse.
En résumé
fetch() est une base solide pour les requêtes HTTP en JavaScript, mais
les paramètres par défaut omettent tout ce dont vous avez besoin en production. Vérifiez toujours response.ok
avant de parser. Ajoutez des délais d'expiration avec AbortController. Construisez une fonction wrapper légère
pour gérer les en-têtes d'authentification, la normalisation des erreurs et les redirections 401 en un seul endroit. Ajoutez une logique de nouvelle tentative
avec backoff exponentiel pour les requêtes pouvant échouer de manière transitoire. Ces quatre habitudes font la
différence entre du code de démonstration et du code de production.