Nel mondo dello sviluppo frontend moderno, interagire con le API backend è una routine quotidiana. Che si tratti di recuperare dati per popolare un’interfaccia utente dinamica, inviare informazioni da un form o triggerare azioni sul server, le richieste HTTP sono al centro di molte applicazioni web. In JavaScript, l’API Workspace
è diventata lo standard de facto per gestire queste operazioni in modo moderno e basato sulle Promise.
Tuttavia, le comunicazioni di rete non sono mai esenti da problemi. Errori temporanei, instabilità della connessione dell’utente, sovraccarichi del server o configurazioni errate possono trasformare una richiesta Workspace
di successo in un fallimento. Una gestione errori fetch robusta non è solo una buona pratica di programmazione, ma è fondamentale per costruire applicazioni frontend resilienti, che offrano un’esperienza utente fluida e affidabile, anche in condizioni non ideali.
Ignorare la gestione degli errori può portare a interfacce utente “rotte”, perdita di dati, frustrazione per l’utente e, in ultima analisi, un impatto negativo sulla percezione della qualità del vostro software. In questa guida completa, esploreremo le tecniche base per gestire gli errori con Workspace
e ci addentreremo in strategie più avanzate come il retry automatico e l’exponential back-off, fondamentali per affrontare gli errori transitori e migliorare significativamente l’affidabilità delle vostre applicazioni frontend.
Perché è Cruciale la Gestione Errori Fetch?
L’API Workspace
è uno strumento potente, che sostituisce le più vecchie XMLHttpRequest
offrendo una sintassi più pulita e un approccio basato sulle Promise per gestire le richieste HTTP. Questo rende il codice asincrono più leggibile, soprattutto in combinazione con async/await
. Ma proprio la natura asincrona e di rete delle operazioni Workspace
introduce potenziali punti di fallimento.
Immaginate un utente che naviga sulla vostra applicazione web. Un componente tenta di caricare dati vitali tramite Workspace
. Cosa succede se la rete dell’utente ha un micro-interruzione? O se il server backend sta subendo un picco di carico e risponde lentamente o con un errore temporaneo (come un 503 Service Unavailable)? Senza un’adeguata gestione degli errori, la richiesta fallirà silenziosamente o genererà un errore non gestito che potrebbe bloccare l’esecuzione dello script JavaScript, lasciando l’utente con un’interfaccia incompleta o non funzionante.
Una buona gestione errori fetch mira a:
- Migliorare l’Esperienza Utente (UX): Fornire feedback chiari all’utente in caso di problemi (es. “Impossibile caricare i dati, riprova più tardi”) anziché lasciare l’interfaccia vuota o bloccata. Strategie come il retry automatico possono addirittura risolvere problemi temporanei senza che l’utente se ne accorga.
- Aumentare l’Affidabilità: Rendere l’applicazione più resistente a problemi di rete o server transitori, aumentando la probabilità che le operazioni vengano completate con successo.
- Facilitare il Debug: Catturare e registrare gli errori in modo strutturato aiuta a identificare e risolvere i problemi più rapidamente.
- Migliorare la Sicurezza (Indirettamente): Sebbene non sia una misura di sicurezza diretta, una corretta gestione degli errori impedisce che informazioni sensibili sugli errori del backend vengano esposte accidentalmente all’utente finale.
Comprendere i diversi tipi di errori che possono verificarsi è il primo passo per gestirli efficacemente.
Tipologie Comuni di Errori con Fetch
Quando si lavora con Workspace
, si possono incontrare diverse categorie di errori, ognuna delle quali viene gestita in modo leggermente diverso dall’API. È fondamentale distinguere tra un errore di rete che fa fallire la richiesta (rejection della Promise) e un errore HTTP (come un 404 o 500) dove la richiesta viene completata, ma il server risponde con uno status code che indica un problema.
Ecco una panoramica dei tipi di errori più comuni:
- Errori di Rete (Network Errors):
- Questi sono gli errori più “profondi”, che impediscono alla richiesta di completarsi a livello di rete.
- Esempi includono: utente offline, timeout della connessione, problemi DNS, blocchi CORS (
Cross-Origin Resource Sharing
) se il server non è configurato correttamente, o interruzione della connessione durante il download. - Quando si verifica un errore di rete, la Promise restituita da
Workspace
viene rifiutata (rejected
). Il blocco.catch()
otry...catch
(conasync/await
) è il posto giusto per intercettare questi errori. L’errore sarà tipicamente unTypeError
. - Per approfondire le interazioni con le API e i potenziali problemi di CORS, potrebbe essere utile consultare guide sulla REST API WordPress: introduzione per frontend headless o, per aspetti più generici sulla sicurezza, La Sicurezza Frontend nel 2025: Guida Pratica su XSS e CORS.
- Errori HTTP (Status Code 4xx e 5xx):
- Questi errori si verificano quando la richiesta raggiunge il server e il server risponde, ma con uno status code che indica un problema.
Workspace
, per impostazione predefinita, non considera uno status code 4xx o 5xx come un errore di rete che causa la rejection della Promise. La Promise viene risolta (resolved
) anche in presenza di questi status.- È responsabilità dello sviluppatore controllare la proprietà
response.ok
(che ètrue
per status code 200-299) oresponse.status
dopo che la Promise è stata risolta per determinare se la richiesta HTTP ha avuto successo a livello applicativo. - Esempi comuni:
400 Bad Request
: La richiesta non è valida.401 Unauthorized
: Autenticazione fallita.403 Forbidden
: L’utente non ha i permessi.404 Not Found
: La risorsa richiesta non esiste.408 Request Timeout
: Il server ha impiegato troppo tempo.422 Unprocessable Entity
: La richiesta è sintatticamente corretta ma semanticamente errata (comune nelle API REST per errori di validazione).500 Internal Server Error
: Errore generico lato server.502 Bad Gateway
: Un server ha ricevuto una risposta non valida da un upstream server.503 Service Unavailable
: Il server non è momentaneamente disponibile (es. sovraccarico, manutenzione).504 Gateway Timeout
: Un server ha atteso troppo a lungo una risposta da un upstream server.
- La gestione di questi errori avviene tipicamente nel primo
.then()
o dopo l’attesa (await
) della Promise diWorkspace
.
- Errori di Timeout (Client-Side):
Workspace
di per sé non ha un’opzione di timeout integrata come le vecchieXMLHttpRequest
.- Per implementare un timeout lato client, è necessario usare l’API
AbortController
in combinazione consetTimeout
. Se la richiesta non si completa entro un certo tempo, viene abortita, e questo triggera una rejection della Promise diWorkspace
con un errore (spesso unAbortError
). - Implementare correttamente i timeout è cruciale per evitare che l’applicazione rimanga bloccata indefinitamente in attesa di una risposta da un server non responsivo.
- Errori di Parsing (es. JSON):
- Dopo una richiesta
Workspace
di successo (status 2xx), è comune parsare il corpo della risposta, ad esempio come JSON (response.json()
). - Se il corpo della risposta non è un JSON valido, il metodo
response.json()
restituirà una Promise che viene rifiutata. - Questi errori devono essere gestiti nel secondo
.then()
(dopo la chiamata aresponse.json()
) o in un bloccocatch
separato se si usaasync/await
.
- Dopo una richiesta
Comprendere dove e come Workspace
segnala questi diversi tipi di errore è il fondamento per una corretta gestione errori fetch.
Tecniche Base di Gestione Errori
Prima di addentrarci in tecniche avanzate come il retry, vediamo i metodi standard per gestire gli errori con Workspace
.
Gestione con Promise (.then().catch()
)
L’approccio tradizionale con le Promise prevede l’uso di .then()
per gestire le risposte e .catch()
per intercettare i rifiuti della Promise (principalmente errori di rete o errori lanciati esplicitamente nei .then()
precedenti).
fetch('https://api.example.com/data')
.then(response => {
// La Promise di fetch si è risolta. La richiesta è arrivata al server.
// Ora controlliamo lo status code HTTP
if (!response.ok) {
// Se lo status non è 2xx, lanciamo un errore.
// Questo verrà catturato dal blocco .catch() successivo.
console.error('Errore HTTP:', response.status, response.statusText);
throw new Error(`Errore HTTP: ${response.status} ${response.statusText}`);
}
// La risposta è OK (status 2xx). Possiamo parsare il corpo.
return response.json();
})
.then(data => {
// Dati parsati con successo
console.log('Dati ricevuti:', data);
// Utilizza i dati...
})
.catch(error => {
// Questo blocco cattura:
// 1. Errori di rete (Promise rejected da fetch)
// 2. Errori lanciati nel primo .then() (es. per status non-OK)
// 3. Errori di parsing (Promise rejected da response.json())
console.error('Si è verificato un errore durante la richiesta:', error);
// Informa l'utente, mostra un messaggio di errore, ecc.
});
In questo esempio, il primo .then()
controlla lo status HTTP. Se non è nel range 200-299 (!response.ok
), lanciamo un nuovo errore. Questo errore viene poi catturato dal blocco .catch()
. Il blocco .catch()
cattura anche gli errori di rete iniziali che impediscono alla Promise di Workspace
di risolversi del tutto, e gli errori durante il parsing del JSON.
Per un’introduzione più generale sull’uso di Workspace
per le chiamate API, potreste trovare utile la guida “Come gestire le richieste API con Fetch in JavaScript“.
Gestione con Async/Await e Try/Catch
L’approccio moderno con async/await
rende il codice asincrono molto simile al codice sincrono e permette di utilizzare la classica struttura try...catch
per gestire sia i rifiuti della Promise di Workspace
(errori di rete) sia gli errori che lanciamo noi (es. per status HTTP non-OK).
async function fetchData(url) {
try {
const response = await fetch(url);
// La Promise di fetch si è risolta. Controlliamo lo status HTTP.
if (!response.ok) {
console.error('Errore HTTP:', response.status, response.statusText);
// Anche qui, lanciamo un errore per status non-OK.
// Questo verrà catturato dal blocco catch.
throw new Error(`Errore HTTP: ${response.status} ${response.statusText}`);
}
// La risposta è OK. Parsiamo il corpo.
const data = await response.json();
console.log('Dati ricevuti:', data);
return data; // Restituisce i dati per l'utilizzo
} catch (error) {
// Questo blocco cattura:
// 1. Errori di rete (Promise rejected da fetch)
// 2. Errori lanciati nel blocco try (es. per status non-OK)
// 3. Errori di parsing (Promise rejected da response.json(), se awaitata nel try)
console.error('Si è verificato un errore durante la richiesta:', error);
// Gestisci l'errore: mostra un messaggio, ecc.
throw error; // Rilancia l'errore se necessario
}
}
// Esempio di utilizzo:
fetchData('https://api.example.com/data')
.then(data => {
// Elabora i dati
})
.catch(error => {
// Gestisci l'errore finale se fetchData lo rilancia
});
// O all'interno di un'altra funzione async:
async function loadAndDisplayData() {
try {
const data = await fetchData('https://api.example.com/data');
// Mostra i dati nell'interfaccia utente
} catch (error) {
// Mostra un messaggio di errore all'utente
console.error('Errore nel caricamento dei dati:', error);
}
}
L’uso di async/await
con try...catch
è generalmente preferito per la sua leggibilità e per come unifica la gestione dei diversi tipi di errori. Se non siete ancora completamente a vostro agio con questo pattern, vi consiglio di consultare la guida “Async Await in JavaScript: Guida Pratica” e “Perché JavaScript è asincrono?” per comprendere meglio i concetti sottostanti.
Queste tecniche base coprono la maggior parte degli scenari di gestione errori immediata. Tuttavia, per errori temporanei, il semplice catch e display di un messaggio non offre la migliore UX. È qui che entrano in gioco il retry automatico e l’exponential back-off.
Introduzione al Concetto di Retry Automatico
Nella realtà delle applicazioni web, non tutti gli errori sono permanenti. A volte, una richiesta fallisce a causa di un problema momentaneo: una momentanea congestione di rete, un micro-riavvio del server API, un database temporaneamente non disponibile. In questi casi, un semplice retry della stessa richiesta dopo un breve intervallo di tempo potrebbe portare al successo.
Il retry automatico è una strategia di gestione errori fetch che consiste nel ripetere automaticamente una richiesta fallita un numero limitato di volte, sperando che le condizioni temporanee che hanno causato il fallimento si risolvano nel frattempo.
Quando e Perché Usare il Retry?
Il retry è particolarmente utile per gestire gli errori transitori. Questi sono errori che ci si aspetta possano risolversi da soli in un breve periodo. Esempi tipici di errori transitori per cui il retry è appropriato includono:
503 Service Unavailable
: Il server è temporaneamente sovraccarico o in manutenzione.504 Gateway Timeout
: La richiesta ha atteso troppo a lungo.- Errori di rete generici (
TypeError
) che non indicano un problema fondamentale (es. una momentanea perdita di pacchetti). 408 Request Timeout
: Simile a 504, ma avviato dal server.
Quando Non Usare il Retry?
È cruciale non implementare il retry per errori permanenti. Ritentare indefinitamente una richiesta che fallisce per un motivo strutturale non risolverà il problema e può solo peggiorare le cose (sprecare risorse del client e del server, peggiorare la UX). Errori permanenti includono:
400 Bad Request
: La richiesta stessa è formattata male. Non cambierà ritentando.401 Unauthorized
,403 Forbidden
: Problemi di autenticazione/autorizzazione. Il retry non risolverà i permessi mancanti.404 Not Found
: La risorsa non esiste. Non esisterà ritentando.422 Unprocessable Entity
: Errore di validazione dei dati inviati. Devi correggere i dati, non ritentare.- Errori applicativi lato server che indicano un bug nel codice backend piuttosto che un problema infrastrutturale temporaneo (anche se a volte un
500 Internal Server Error
può essere transitorio, è più rischioso ritentarlo senza distinguere la causa).
L’obiettivo del retry è migliorare l’affidabilità senza richiedere l’intervento dell’utente e senza sovraccaricare inutilmente i sistemi.
Come Implementare un Sistema di Retry Semplice
Implementare un sistema di retry base è relativamente semplice. Possiamo creare una funzione wrapper attorno a Workspace
che tenti la richiesta più volte in caso di errore, con un breve intervallo tra i tentativi.
Useremo un approccio ricorsivo asincrono, che è elegante e si presta bene alla gestione delle Promise.
/**
* Esegue una richiesta fetch con un numero limitato di retry.
* @param {string} url L'URL della risorsa da fetchare.
* @param {Object} options Le opzioni per la richiesta fetch (method, headers, body, ecc.).
* @param {number} maxRetries Il numero massimo di tentativi (inclusa la prima richiesta).
* @param {number} delay Il tempo di attesa in millisecondi tra un tentativo e l'altro.
* @returns {Promise<Response>} Una Promise che si risolve con la Response o si rifiuta dopo i retry.
*/
async function fetchWithSimpleRetry(url, options = {}, maxRetries = 3, delay = 1000) {
try {
const response = await fetch(url, options);
// Controlla se la risposta HTTP è "OK" (status 2xx)
if (response.ok) {
return response; // Successo! Restituisci la risposta.
} else {
// La richiesta è arrivata al server, ma con uno status non-OK.
// Decidi se questo status merita un retry (es. 5xx)
// Questo è un punto cruciale: NON ritentare per errori permanenti (4xx)
const retryableStatusCodes = [408, 429, 500, 502, 503, 504]; // Esempi comuni
if (maxRetries > 1 && retryableStatusCodes.includes(response.status)) {
console.warn(`Tentativo fallito (${response.status}). Ritento... Tentativi rimanenti: ${maxRetries - 1}`);
await new Promise(resolve => setTimeout(resolve, delay)); // Attende il delay
// Chiama ricorsivamente la funzione con un retry in meno
return fetchWithSimpleRetry(url, options, maxRetries - 1, delay);
} else {
// Status non retriabile o tentativi esauriti
console.error(`Richiesta fallita dopo ${3 - maxRetries + 1} tentativi (ultimo status: ${response.status}).`);
// Rilancia l'errore originale o crea uno nuovo
throw new Error(`Richiesta fallita con status ${response.status} dopo diversi tentativi.`);
}
}
} catch (error) {
// Questo catch gestisce gli errori di rete (Promise rejected da fetch).
// Generalmente, gli errori di rete sono considerati transitori e retriabili.
if (maxRetries > 1) {
console.warn(`Errore di rete: ${error.message}. Ritento... Tentativi rimanenti: ${maxRetries - 1}`);
// Implementazione Timeout: Se l'errore è dovuto a un AbortError (timeout), non ha senso aspettare l'intero delay.
// Potremmo gestire questo caso specificamente se usiamo AbortController per il timeout.
// Per semplicità qui, aspettiamo sempre il delay.
await new Promise(resolve => setTimeout(resolve, delay)); // Attende il delay
// Chiama ricorsivamente la funzione con un retry in meno
return fetchWithSimpleRetry(url, options, maxRetries - 1, delay);
} else {
// Tentativi esauriti o errore non retriabile (molto rari per i TypeErrors iniziali)
console.error(`Richiesta fallita dopo ${3 - maxRetries + 1} tentativi. Ultimo errore: ${error.message}`);
throw error; // Rilancia l'errore originale
}
}
}
// Esempio di utilizzo:
async function loadDataWithRetry() {
try {
const response = await fetchWithSimpleRetry('https://api.example.com/risorsa-potenzialmente-instabile', { method: 'GET' }, 5, 2000); // 5 tentativi totali, 2 secondi di delay
// Dopo il successo (o il fallimento gestito internamente), parsiamo il JSON
const data = await response.json();
console.log('Dati caricati con retry:', data);
// Processa i dati...
} catch (error) {
console.error('Caricamento dati fallito dopo tutti i retry:', error);
// Mostra un messaggio di errore all'utente
}
}
// loadDataWithRetry();
Questo esempio implementa un retry semplice con un ritardo fisso tra i tentativi. Controlla la proprietà response.ok
per gli errori HTTP e cattura gli errori di rete. È configurabile con il numero massimo di tentativi e il ritardo.
Considerazioni per il Retry Semplice:
- Delay Fisso: Il problema principale di un ritardo fisso è che, in caso di guasti diffusi o riavvii del server, molti client che ritentano con lo stesso intervallo potrebbero “sincronizzarsi” e bombardare il server simultaneamente, peggiorando la situazione.
- Quando Ritentare gli Status HTTP: La lista
retryableStatusCodes
è un esempio. Dovreste adattarla in base alla vostra conoscenza specifica dell’API e dei tipi di errori transitori che può restituire. Generalmente si evitano i 4xx (errori client-side) e si considerano i 5xx (errori server-side) come potenzialmente retriabili.
Per un frontend più robusto, è preferibile una strategia di retry più sofisticata: l’exponential back-off.
Introduzione al Retry con Exponential Back-off
Come accennato, un ritardo fisso tra i retry può portare a problemi di “thundering herd”, dove molti client ritentano contemporaneamente, sovraccaricando ulteriormente un server già in difficoltà. L’exponential back-off è una strategia di retry che risolve questo problema aumentando gradualmente il tempo di attesa tra i tentativi successivi.
L’idea base è semplice:
- Primo tentativo fallisce.
- Attendi
X
secondi e ritenta. - Se fallisce di nuovo, attendi
X * factor
secondi e ritenta. - Se fallisce ancora, attendi
X * factor^2
secondi e ritenta. - E così via, con il ritardo che cresce esponenzialmente con ogni tentativo fallito.
Questo approccio presenta diversi vantaggi:
- Riduce il Carico sul Server: I tentativi si diradano nel tempo, dando al server più “respiro” per recuperare.
- Evita Sincronizzazioni: Difficile che molti client con la stessa strategia di exponential back-off finiscano per ritentare esattamente nello stesso momento.
- Migliora la Probabilità di Successo: Dando più tempo al server o alla rete per stabilizzarsi, i tentativi successivi hanno una maggiore probabilità di successo.
Jitter (Variazione Casuale)
Anche con l’exponential back-off, è possibile che un gran numero di client inizi i propri retry all’incirca nello stesso momento, specialmente se tutti hanno fallito a causa dello stesso evento (es. un riavvio del server). Per mitigare ulteriormente questo rischio, si aggiunge spesso una piccola variazione casuale, chiamata jitter, al tempo di attesa calcolato.
La formula per il ritardo con exponential back-off e jitter potrebbe essere:
delay = min(maxDelay, initialDelay * factor^n + random_jitter)
Dove:
initialDelay
: Il ritardo base per il primo retry.factor
: Il moltiplicatore (comunemente 2).n
: Il numero del tentativo di retry (a partire da 1 per il primo retry dopo il primo fallimento).random_jitter
: Un valore casuale tra 0 edelay
(o una frazione di esso).maxDelay
: Un limite superiore per il ritardo, per evitare attese eccessivamente lunghe.
Questo aggiunge un elemento di imprevedibilità che distribuisce ulteriormente i tentativi nel tempo.
Implementazione del Retry con Exponential Back-off (Esempio di Codice)
Ora, adattiamo la nostra funzione di retry semplice per implementare l’exponential back-off con jitter.
/**
* Attende per un certo numero di millisecondi.
* @param {number} ms Il tempo di attesa in millisecondi.
* @returns {Promise<void>} Una Promise che si risolve dopo il tempo specificato.
*/
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Esegue una richiesta fetch con retry e exponential back-off.
* @param {string} url L'URL della risorsa da fetchare.
* @param {Object} options Le opzioni per la richiesta fetch.
* @param {Object} retryOptions Opzioni specifiche per il retry.
* @param {number} [retryOptions.maxAttempts=3] Il numero massimo di tentativi (inclusa la prima richiesta).
* @param {number} [retryOptions.initialDelay=1000] Il ritardo iniziale in millisecondi per il primo retry.
* @param {number} [retryOptions.factor=2] Il fattore di moltiplicazione per l'exponential back-off.
* @param {number} [retryOptions.maxDelay=30000] Il ritardo massimo consentito tra i retry (30 secondi).
* @param {boolean} [retryOptions.useJitter=true] Indica se aggiungere jitter casuale.
* @param {number[]} [retryOptions.retryableStatusCodes=[408, 429, 500, 502, 503, 504]] Status code HTTP per cui tentare il retry.
* @returns {Promise<Response>} Una Promise che si risolve con la Response o si rifiuta dopo i retry.
*/
async function fetchWithExponentialBackoff(url, options = {}, retryOptions = {}) {
const {
maxAttempts = 3,
initialDelay = 1000, // 1 secondo
factor = 2,
maxDelay = 30000, // 30 secondi
useJitter = true,
retryableStatusCodes = [408, 429, 500, 502, 503, 504]
} = retryOptions;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const response = await fetch(url, options);
// Successo HTTP (status 2xx)
if (response.ok) {
return response;
}
// La richiesta è arrivata al server, ma con uno status non-OK.
// Controlla se lo status code è nella lista di quelli retriabili
if (!retryableStatusCodes.includes(response.status)) {
console.error(`Richiesta fallita con status non retriabile ${response.status} al tentativo ${attempt}. Nessun ulteriore retry.`);
// Non ritentare per errori permanenti
throw new Error(`Richiesta fallita con status ${response.status}.`);
}
// Se siamo qui, lo status è retriabile, ma non OK.
if (attempt < maxAttempts) {
// Calcola il ritardo per il prossimo tentativo
let delay = initialDelay * Math.pow(factor, attempt - 1); // initialDelay * factor^(tentativo - 1)
if (useJitter) {
// Aggiunge jitter: un valore casuale tra 0 e il ritardo calcolato
delay = delay + Math.random() * delay;
}
delay = Math.min(delay, maxDelay); // Applica il ritardo massimo
console.warn(`Tentativo ${attempt} fallito (${response.status}). Attendo ${Math.round(delay)}ms prima del prossimo retry.`);
await wait(delay); // Attende
} else {
// Ultimo tentativo fallito con status retriabile
console.error(`Tentativo ${attempt} fallito (${response.status}). Tentativi esauriti.`);
throw new Error(`Richiesta fallita con status ${response.status} dopo ${maxAttempts} tentativi.`);
}
} catch (error) {
// Questo catch gestisce gli errori di rete (Promise rejected da fetch)
// o errori lanciati nel try (come AbortError per timeout).
// Generalmente, questi sono considerati retriabili.
if (attempt < maxAttempts) {
let delay = initialDelay * Math.pow(factor, attempt - 1);
if (useJitter) {
delay = delay + Math.random() * delay;
}
delay = Math.min(delay, maxDelay);
console.warn(`Tentativo ${attempt} fallito (errore di rete/catch): ${error.message}. Attendo ${Math.round(delay)}ms prima del prossimo retry.`);
await wait(delay); // Attende
} else {
// Ultimo tentativo fallito con errore di rete/catch
console.error(`Tentativo ${attempt} fallito (errore di rete/catch): ${error.message}. Tentativi esauriti.`);
throw error; // Rilancia l'errore originale
}
}
}
// Questo punto non dovrebbe essere raggiunto se maxAttempts > 0,
// ma per completezza possiamo lanciare un errore finale qui se necessario.
// throw new Error(`Richiesta fallita dopo ${maxAttempts} tentativi.`);
}
// Esempio di utilizzo con AbortController per il timeout e retry:
async function fetchWithTimeoutAndRetry(url, options = {}, retryOptions = {}, timeoutMs = 5000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs); // Imposta il timer per abortire
try {
// Aggiungi il segnale AbortController alle opzioni fetch
const response = await fetchWithExponentialBackoff(url, {
...options,
signal: controller.signal
}, retryOptions);
clearTimeout(id); // Annulla il timer se la fetch ha successo
return response;
} catch (error) {
clearTimeout(id); // Annulla il timer anche in caso di errore
// L'errore potrebbe essere un AbortError se il timeout è scattato.
// fetchWithExponentialBackoff gestisce già i retry per errori di catch (inclusi AbortError).
console.error('Fetch finale fallita dopo timeout/retry:', error);
throw error; // Rilancia l'errore per la gestione esterna
}
}
// Esempio di utilizzo della funzione avanzata:
async function loadDataWithAdvancedRetry() {
try {
const response = await fetchWithTimeoutAndRetry(
'https://api.example.com/risorsa-molto-instabile',
{ method: 'GET' }, // Opzioni Fetch standard
{ maxAttempts: 5, initialDelay: 500, factor: 2, maxDelay: 10000, useJitter: true }, // Opzioni Retry
7000 // Timeout per ogni singolo tentativo fetch (7 secondi)
);
const data = await response.json();
console.log('Dati caricati con successo dopo retry e back-off:', data);
// Processa i dati...
} catch (error) {
console.error('Caricamento dati fallito definitivamente dopo tutti i retry/timeout:', error);
// Mostra un messaggio di errore all'utente, suggerisci di riprovare manualmente
}
}
// loadDataWithAdvancedRetry();
Questo codice dimostra un’implementazione più robusta:
- Utilizza un loop
for
per tenere traccia dei tentativi. - Include una funzione
wait
basata susetTimeout
e Promise per creare ritardi asincroni non bloccanti. - Calcola il ritardo per ogni tentativo utilizzando l’exponential back-off (
initialDelay * factor^(attempt - 1)
). - Aggiunge il jitter moltiplicando il ritardo calcolato per un valore casuale tra 0 e 1.
- Applica un
maxDelay
per evitare attese eccessivamente lunghe. - Migliora la logica di gestione degli errori HTTP, ritentando solo per status code specifici considerati transitori.
- Integra
AbortController
esetTimeout
per gestire i timeout per ciascun tentativo diWorkspace
.
Questa funzione WorkspaceWithExponentialBackoff
(o WorkspaceWithTimeoutAndRetry
che la wrappa) è un blocco riutilizzabile potente per aggiungere resilienza alle vostre chiamate API.
Migliori Pratiche nella Gestione Errori Fetch con Retry
L’implementazione di retry e exponential back-off è una tecnica potente, ma deve essere usata con criterio. Ecco alcune migliori pratiche da seguire:
- Definire un Massimo Numero di Retry: Impostare un limite massimo di tentativi (
maxAttempts
) è fondamentale. Un numero troppo alto può causare attese eccessive per l’utente in caso di errori permanenti e sprecare risorse. Un numero ragionevole dipende dal contesto, ma 3-5 tentativi sono spesso sufficienti per la maggior parte degli errori transitori tipici del web. Se dopo 5 tentativi non funziona, è probabile che il problema non sia transitorio. - Identificare e Non Ritentare Errori Critici/Permanenti: Come discusso, non ha senso ritentare indefinitamente errori come
400 Bad Request
,401 Unauthorized
,403 Forbidden
,404 Not Found
,422 Unprocessable Entity
. Questi errori indicano problemi con la richiesta stessa o con lo stato dell’applicazione, non problemi temporanei. La vostra logica di retry deve escludere esplicitamente questi status code. - Fornire Feedback all’Utente: Anche con il retry automatico, l’utente potrebbe notare un rallentamento o, peggio, la richiesta potrebbe fallire dopo tutti i tentativi. È essenziale fornire un feedback visivo:
- Indicatore di caricamento durante la richiesta (e i retry).
- Messaggio chiaro se la richiesta fallisce definitivamente (“Impossibile caricare i dati. Riprova più tardi.” o “Verifica la tua connessione.”).
- In alcuni casi, un pulsante “Riprova Ora” esplicito può dare all’utente il controllo in caso di fallimento finale. Evitate spinner infiniti.
- Implementare Timeout Appropriati: Combinare il retry con un timeout per ogni singolo tentativo (
AbortController
) è cruciale. Se un server non risponde affatto, non vogliamo che un singolo tentativo rimanga bloccato per minuti prima che il retry successivo abbia luogo. Il timeout dovrebbe essere ragionevole per l’operazione (es. pochi secondi). - Logging ed Error Reporting: Anche con il retry, alcuni errori persisteranno. Implementare un sistema di logging lato client (es. inviare errori a un servizio come Sentry, Rollbar o una soluzione custom) è vitale per monitorare la salute dell’applicazione, identificare pattern di errore e debuggare problemi che gli utenti stanno riscontrando.
- Attenzione all’Idempotenza: Siate cauti nel ritentare richieste che non sono idempotenti. Una richiesta è idempotente se può essere eseguita più volte senza cambiare il risultato oltre la prima esecuzione. Le richieste
GET
,HEAD
,PUT
(aggiornamento completo di una risorsa) eDELETE
sono tipicamente idempotenti. Le richiestePOST
(creazione di una nuova risorsa) oPATCH
(aggiornamento parziale) non lo sono necessariamente. Ritentare unaPOST
fallita senza una logica aggiuntiva potrebbe accidentalmente creare risorse duplicate sul server se la richiesta originale è fallita dopo che il server l’ha elaborata ma prima che il client ricevesse la risposta di successo. Per operazioni non idempotenti, potrebbe essere necessario un approccio diverso o una gestione lato server che rilevi e gestisca i retry duplicati (es. usando un ID di idempotenza). - Adattare la Strategia al Contesto: La strategia di retry (numero di tentativi, ritardo iniziale, fattore di back-off, status code retriabili) dovrebbe essere calibrata in base alla specificità dell’API chiamata, alla sua prevedibile affidabilità e al tipo di operazione. Un’operazione critica potrebbe meritare più tentativi di un’operazione meno importante.
Seguendo queste migliori pratiche, potrete implementare una gestione errori fetch con retry e exponential back-off che sia efficace, rispettosa dei server backend e ottimale per l’esperienza utente.
Vantaggi: Migliorare UX e SEO
Implementare tecniche avanzate come il retry con exponential back-off nella tua gestione errori fetch porta benefici tangibili sia per l’Esperienza Utente (UX) che, indirettamente, per la Search Engine Optimization (SEO).
Impatto sulla UX:
- Fluidità e Percezione di Velocità: Quando un errore è transitorio e viene gestito con un retry di successo, l’utente non si accorge nemmeno del problema iniziale. La richiesta semplicemente impiega un po’ più di tempo. Questo contribuisce a una percezione di maggiore velocità e reattività dell’applicazione rispetto a un fallimento immediato che richiede l’intervento manuale dell’utente (come ricaricare la pagina).
- Affidabilità Percepita: Un’applicazione che non “si rompe” o non mostra messaggi di errore per problemi temporanei appare molto più stabile e affidabile agli occhi dell’utente. Questo aumenta la fiducia e la soddisfazione.
- Meno Frustrazione: Evitare che gli utenti incontrino schermate vuote o messaggi di errore frustranti per problemi al di fuori del loro controllo migliora significativamente l’usabilità e riduce il tasso di abbandono.
Impatto sulla SEO (Indiretto):
Sebbene le chiamate Workspace
siano eseguite lato client (nel browser dell’utente) e non influenzino direttamente il contenuto visto dai crawler dei motori di ricerca durante il crawling iniziale (a meno che non si utilizzi rendering lato server o pre-rendering che esegue queste chiamate), una robusta gestione degli errori ha un impatto indiretto e crescente sulla SEO:
- Core Web Vitals e User Experience: Google e altri motori di ricerca pongono sempre più enfasi sulla user experience come fattore di ranking, misurata in parte dai Core Web Vitals e altri segnali di performance. Un’applicazione che fallisce nel caricare contenuti chiave tramite
Workspace
a causa di errori non gestiti porta a:- Poor LCP (Largest Contentful Paint): Se il contenuto principale dipende da una chiamata
Workspace
fallita, il LCP potrebbe non completarsi mai correttamente o mostrare uno stato di errore, influenzando negativamente questo indicatore. Per ottimizzare l’LCP, è cruciale che i contenuti principali vengano caricati velocemente e in modo affidabile. Potrebbe essere utile consultare Ottimizzare LCP per siti più veloci. - High Bounce Rate: Utenti frustrati da errori o interfacce non funzionanti abbandoneranno il sito rapidamente. Un alto tasso di abbandono è un segnale negativo per i motori di ricerca.
- Low Dwell Time: Se gli utenti non possono interagire con il contenuto o le funzionalità a causa di errori, il tempo trascorso sulla pagina diminuirà.
- Poor LCP (Largest Contentful Paint): Se il contenuto principale dipende da una chiamata
- Affidabilità Generale del Sito: I motori di ricerca premiano i siti web che offrono un’esperienza utente stabile e affidabile. Una gestione errori fetch efficace contribuisce a questa percezione generale di qualità del sito.
- Miglioramento Continuo: Implementare retry e back-off è parte di una cultura di sviluppo che mira all’affidabilità e alle performance, elementi che sono intrinsecamente legati alla SEO tecnica moderna. Per una panoramica più ampia sulla SEO tecnica per developer, questa pratica si inserisce perfettamente.
In sintesi, una gestione errori fetch avanzata che incorpora retry e exponential back-off non solo rende le vostre applicazioni più resistenti e piacevoli da usare, ma contribuisce anche indirettamente a migliorare la visibilità sui motori di ricerca assicurando che gli utenti (e potenzialmente i crawler che eseguono JavaScript in futuro) possano accedere e interagire con il contenuto in modo affidabile.
Conclusione
La gestione errori fetch è un aspetto non negoziabile nello sviluppo frontend moderno. Affidarsi alla semplice gestione di base con .catch()
o try...catch
è un punto di partenza, ma non è sufficiente per costruire applicazioni resilienti in un ambiente web intrinsecamente instabile.
Abbiamo visto come distinguere i vari tipi di errori che Workspace
può incontrare, dagli errori di rete agli status HTTP non-OK, e come le tecniche base ci aiutano a intercettarli. Tuttavia, per affrontare efficacemente gli errori transitori, il retry automatico si rivela una strategia potente.
Per portare questa strategia a un livello superiore e prevenire problemi di sovraccarico sui server, l’implementazione del retry con exponential back-off e l’aggiunta di jitter sono pratiche consigliate. Queste tecniche aumentano gradualmente il tempo di attesa tra i tentativi falliti, distribuendo il carico e aumentando le probabilità di successo nel tempo.
Ricordate sempre di definire un numero massimo di tentativi, di non ritentare errori permanenti e di fornire feedback chiaro all’utente. Combinare queste strategie con timeout appropriati (tramite AbortController
) garantisce che le vostre richieste non rimangano appese indefinitamente.
Investire tempo nella creazione di una robusta gestione errori fetch con retry e exponential back-off è un investimento nella qualità della vostra applicazione, che si traduce in una migliore esperienza utente, una maggiore affidabilità e, in definitiva, un impatto positivo (sebbene indiretto) sulla SEO. Costruire frontend resilienti è una competenza chiave per gli sviluppatori moderni, e padroneggiare queste tecniche vi distinguerà.
Continuate a esplorare le best practice dello sviluppo frontend consultando altre risorse come la guida su 5 errori comuni nei progetti frontend e come evitarli.