fetch() è ora integrato in ogni browser moderno e in Node.js 18+. Ha sostituito XMLHttpRequest ed eliminato la necessità di Axios nella maggior parte dei progetti. Ma l'utilizzo predefinito che ogni tutorial mostra — fetch(url).then(r => r.json()) — salta la gestione degli errori, non ha timeout e crolla in qualsiasi ambiente di produzione reale. Questa guida copre i pattern che reggono davvero.

Le Basi — GET e POST

fetch() restituisce una Promise che si risolve con un oggetto Response. Una richiesta GET è semplice:

js
const response = await fetch('https://api.example.com/products');
const products = await response.json();

Una richiesta POST con un corpo JSON richiede un po' più di configurazione:

js
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}`);
Errore comune: dimenticare Content-Type: application/json nelle richieste POST. Senza di esso, molti framework server non analizzeranno il corpo, e riceverai un 400 Bad Request o un corpo della richiesta vuoto senza alcun messaggio di errore utile.

Il Controllo Errori in Due Passaggi — response.ok

Questo è il pattern più importante da interiorizzare. fetch() rifiuta la sua Promise solo in caso di errori di rete (nessuna connessione, errore DNS, blocco CORS). Una risposta 404, 401 o 500 risolve comunque la Promise — con response.ok impostato a false. Se non controlli questo, passerai silenziosamente le risposte di errore a response.json():

js
// ❌ 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 è true per i codici di stato 200–299. Qualsiasi altra cosa — reindirizzamenti 301 (se non seguiti automaticamente), errori 400, errori 500 — lo imposta a false. Controllalo sempre prima di analizzare.

AbortController — Timeout e Cancellazione

fetch() non ha un timeout integrato. Una richiesta può bloccarsi indefinitamente se il server smette di rispondere a metà trasferimento. La soluzione è AbortController:

js
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 è utile anche per cancellare richieste in corso quando un utente naviga altrove o esegue una nuova ricerca prima che quella precedente sia completata:

js
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
  }
}

Retry con Backoff Esponenziale

Le richieste di rete falliscono in modo transitorio — un 503 a un retry spesso riesce al successivo. Il backoff esponenziale è la strategia standard: attendere progressivamente più a lungo tra i retry per evitare di sovraccaricare un server sovraccarico:

js
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);
Il jitter conta: Aggiungere Math.random() * 100 al ritardo previene il "thundering herd" — dove migliaia di client riprovano tutti nello stesso momento dopo un problema del server. Un piccolo offset casuale, un grande beneficio per l'affidabilità.

Un Pattern Interceptor — Avvolgere fetch()

Axios ha reso popolare il concetto di interceptor: un hook che viene eseguito prima di ogni richiesta e dopo ogni risposta. Puoi costruire la stessa cosa come un sottile wrapper attorno a fetch():

js
// 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 })
});

Questo pattern centralizza l'autenticazione, la gestione degli errori e la configurazione dell'URL base. Ogni chiamata API nel tuo codebase ottiene lo stesso comportamento gratuitamente. Quando i requisiti cambiano — come aggiungere un nuovo header o passare da JWT ai cookie di sessione — aggiorni un solo posto.

Gestione di Diversi Tipi di Risposta

Non ogni API restituisce JSON. fetch() ha metodi per tutti i tipi di risposta comuni:

js
// 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 — Cosa Devono Sapere gli Sviluppatori

CORS (Cross-Origin Resource Sharing) è imposto dal browser, non dal server. Quando il tuo frontend su app.example.com effettua fetch da api.example.com, il browser invia un header Origin e controlla la risposta per i permessi. Ecco cosa controlli lato client:

js
// 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 response

Se stai ricevendo errori CORS, la soluzione è quasi sempre sul server: il server deve aggiungere Access-Control-Allow-Origin ai propri header di risposta. La guida CORS di MDN è il riferimento più completo su come configurarlo.

Strumenti Utili

Quando stai eseguendo il debug delle risposte fetch, JSON Formatter rende leggibili i payload delle API e JSON Validator individua le risposte malformate. Per codificare i parametri di query in modo sicuro, JSON URL Encode fa al caso tuo. La guida MDN Using Fetch è il miglior riferimento unico per tutte le opzioni fetch e i metodi di risposta.

Conclusioni

fetch() è una solida base per le richieste HTTP in JavaScript, ma le impostazioni predefinite tralasciano tutto ciò di cui hai bisogno in produzione. Controlla sempre response.ok prima di analizzare. Aggiungi timeout con AbortController. Costruisci una sottile funzione wrapper per gestire header di autenticazione, normalizzazione degli errori e reindirizzamenti 401 in un unico posto. Aggiungi logica di retry con backoff esponenziale per le richieste che possono fallire in modo transitorio. Queste quattro abitudini fanno la differenza tra codice demo e codice di produzione.