⌨️ Sviluppo web

Fetch e async/await in JavaScript: errori, timeout e tentativi

Usare fetch con async/await nel modo giusto: controllo di response.ok, errori di rete, timeout con AbortController, richieste parallele e nuovi tentativi.

L'API fetch è il modo standard per fare richieste HTTP dal browser, ed è disponibile anche nelle versioni moderne di Node.js. Con async e await il codice asincrono si legge quasi come codice sequenziale. Proprio questa semplicità nasconde però alcune trappole: errori che non vengono intercettati, richieste che restano appese all'infinito, risposte lette due volte. Vediamo come scrivere chiamate robuste.

La forma base

async function caricaArticoli() {
  const risposta = await fetch('/api/articoli');
  const dati = await risposta.json();
  return dati;
}

Funziona, finché va tutto bene. Ma questo codice ha già un difetto importante.

fetch non fallisce sugli errori HTTP

È la sorpresa più comune: la promessa restituita da fetch viene rifiutata solo per errori di rete (server irraggiungibile, connessione interrotta, richiesta bloccata da CORS). Una risposta 404 o 500 è, dal punto di vista di fetch, una risposta riuscita: la promessa si risolve normalmente.

Per questo devi controllare sempre response.ok, che è vero per i codici da 200 a 299:

async function caricaArticoli() {
  const risposta = await fetch('/api/articoli');
  if (!risposta.ok) {
    throw new Error(`Richiesta fallita: ${risposta.status}`);
  }
  return risposta.json();
}

Senza questo controllo, una pagina di errore HTML finirebbe in response.json(), che lancerebbe un errore di sintassi poco chiaro.

Un errore personalizzato più utile

Quando l'API restituisce errori strutturati, conviene conservarli:

class ErroreHttp extends Error {
  constructor(risposta, corpo) {
    super(`HTTP ${risposta.status}`);
    this.status = risposta.status;
    this.corpo = corpo;
  }
}

async function richiestaJson(url, opzioni = {}) {
  const risposta = await fetch(url, {
    ...opzioni,
    headers: { 'Accept': 'application/json', ...opzioni.headers }
  });
  const tipo = risposta.headers.get('content-type') || '';
  const corpo = tipo.includes('application/json')
    ? await risposta.json()
    : await risposta.text();
  if (!risposta.ok) throw new ErroreHttp(risposta, corpo);
  return corpo;
}

Leggiamo il corpo una sola volta: uno stream di risposta si può consumare solo una volta, e chiamare prima json() e poi text() sulla stessa risposta genera un errore. Se ti serve leggerlo due volte, usa risposta.clone().

Gestire gli errori con try/catch

Con await, gli errori si intercettano con un normale try/catch:

try {
  const articoli = await richiestaJson('/api/articoli');
  mostra(articoli);
} catch (errore) {
  if (errore instanceof ErroreHttp && errore.status === 401) {
    reindirizzaAlLogin();
  } else {
    mostraMessaggio('Impossibile caricare gli articoli. Riprova tra poco.');
  }
}

Distinguere i casi permette di dare all'utente messaggi utili: un problema di autorizzazione si risolve in modo diverso da un server temporaneamente non disponibile.

Attenzione alle funzioni asincrone chiamate senza await né .catch(): se falliscono, l'errore diventa un "unhandled rejection" che può passare inosservato.

Timeout con AbortController

fetch non ha un timeout predefinito: se il server non risponde, la richiesta può restare in sospeso molto a lungo. La soluzione è AbortSignal:

const risposta = await fetch('/api/report', {
  signal: AbortSignal.timeout(8000) // 8 secondi
});

Allo scadere del tempo la promessa viene rifiutata con un errore il cui name è TimeoutError. Per annullare manualmente, per esempio quando l'utente cambia pagina o digita una nuova ricerca, usa un AbortController:

let controllore;

async function cerca(testo) {
  controllore?.abort();          // annulla la ricerca precedente
  controllore = new AbortController();
  try {
    const r = await fetch(`/api/cerca?q=${encodeURIComponent(testo)}`, {
      signal: controllore.signal
    });
    return await r.json();
  } catch (e) {
    if (e.name === 'AbortError') return null; // annullata: non è un errore
    throw e;
  }
}

Questo schema evita anche un bug sottile: senza annullamento, una risposta lenta a una ricerca vecchia potrebbe arrivare dopo quella nuova e sovrascriverne i risultati.

Nota anche encodeURIComponent: i valori inseriti in un URL vanno sempre codificati, oppure costruiti con URLSearchParams.

Richieste in parallelo

Se le richieste sono indipendenti, non aspettarle una alla volta:

// Lento: in sequenza
const utente = await richiestaJson('/api/utente');
const notifiche = await richiestaJson('/api/notifiche');

// Più veloce: in parallelo
const [utente2, notifiche2] = await Promise.all([
  richiestaJson('/api/utente'),
  richiestaJson('/api/notifiche')
]);

Promise.all fallisce appena una delle promesse fallisce. Se vuoi tutti i risultati, anche parziali, usa Promise.allSettled, che restituisce per ognuna lo stato (fulfilled o rejected) e il valore o l'errore.

Nuovi tentativi con attesa crescente

Per errori temporanei (rete instabile, 503, 429) può avere senso riprovare, ma con criterio:

async function conTentativi(fn, tentativi = 3) {
  for (let i = 0; ; i++) {
    try {
      return await fn();
    } catch (e) {
      const temporaneo = !(e instanceof ErroreHttp) || [429, 502, 503, 504].includes(e.status);
      if (!temporaneo || i >= tentativi - 1) throw e;
      await new Promise(r => setTimeout(r, 500 * 2 ** i)); // 0,5s, 1s, 2s…
    }
  }
}

Riprova solo le operazioni idempotenti, come le letture: ripetere automaticamente un POST che crea un ordine può duplicarlo. Non riprovare mai gli errori 4xx che indicano un problema della richiesta, perché il risultato non cambierà.

In sintesi

  • fetch rifiuta la promessa solo per errori di rete: controlla sempre response.ok.
  • Leggi il corpo della risposta una sola volta, scegliendo il metodo in base al Content-Type.
  • Usa AbortSignal.timeout per i timeout e AbortController per annullare richieste superate.
  • Esegui in parallelo le richieste indipendenti con Promise.all o Promise.allSettled.
  • Riprova solo errori temporanei e operazioni idempotenti, con attese crescenti.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica, unisce il rigore dell'analisi tecnica all'attenzione per le persone che usano la tecnologia.

Leggi anche