Introduzione
Quando scriviamo codice asincrono in JavaScript, spesso il problema non è solo eseguire un’operazione, ma anche gestire i casi in cui fallisce temporaneamente. Pensiamo a una chiamata HTTP verso un’API che ogni tanto risponde in ritardo, a un servizio esterno che può andare in timeout, oppure a una connessione di rete instabile. In questi casi, una semplice gestione degli errori non basta: serve un meccanismo che provi di nuovo l’operazione in modo controllato.
Un approccio molto utile è il pattern retry con backoff: se un’operazione fallisce, la ritentiamo dopo un’attesa progressivamente più lunga. Questo rende il codice più robusto e, se scritto bene con async/await, anche molto leggibile.
In questo tutorial vedremo come costruire una funzione riutilizzabile che esegue un’operazione asincrona con più tentativi, aspettando un tempo crescente tra un tentativo e l’altro. È un caso pratico, frequente in applicazioni reali, e ottimo per capire come usare async/await in modo pulito.
Codice completo
// Utility: attende un certo numero di millisecondi
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Esegue una funzione asincrona con retry e backoff esponenziale.
*
* @param {Function} operation - Funzione asincrona da eseguire.
* @param {Object} options - Opzioni di retry.
* @param {number} options.retries - Numero massimo di tentativi.
* @param {number} options.initialDelay - Attesa iniziale in ms.
* @param {number} options.factor - Moltiplicatore del backoff.
* @returns {Promise<any>}
*/
async function retryWithBackoff(operation, {
retries = 3,
initialDelay = 300,
factor = 2
} = {}) {
let attempt = 0;
let delayMs = initialDelay;
while (attempt < retries) {
try {
attempt++;
console.log(`Tentativo ${attempt} di ${retries}`);
const result = await operation();
return result;
} catch (error) {
console.log(`Errore nel tentativo ${attempt}:`, error.message);
if (attempt === retries) {
throw new Error(`Operazione fallita dopo ${retries} tentativi: ${error.message}`);
}
console.log(`Riprovo tra ${delayMs}ms...`);
await delay(delayMs);
delayMs *= factor;
}
}
}
// Simulazione di una chiamata API instabile
async function fetchUserProfile() {
const random = Math.random();
// Simula un fallimento nel 60% dei casi
if (random < 0.6) {
throw new Error("Servizio temporaneamente non disponibile");
}
return {
id: 42,
name: "Alice",
role: "Developer"
};
}
// Esempio di utilizzo
(async () => {
try {
const profile = await retryWithBackoff(fetchUserProfile, {
retries: 5,
initialDelay: 500,
factor: 2
});
console.log("Profilo ottenuto:", profile);
} catch (error) {
console.error("Operazione conclusa con errore:", error.message);
}
})(); Spiegazione
Il codice è composto da tre parti principali.
1. La funzione delay
delay(ms) restituisce una Promise che si risolve dopo un certo numero di millisecondi. È un mattoncino fondamentale quando vogliamo inserire una pausa in un flusso asincrono senza bloccare il thread principale.
Grazie ad await delay(delayMs), il codice resta leggibile: sembra quasi una pausa “sincrona”, ma in realtà JavaScript libera il controllo e riprende più tardi.
2. La funzione retryWithBackoff
Questa è il cuore del tutorial. La funzione riceve:
- operation: una funzione asincrona da eseguire;
- retries: il numero massimo di tentativi;
- initialDelay: il tempo di attesa iniziale;
- factor: il moltiplicatore del backoff.
Il ciclo while prova a eseguire l’operazione. Se va a buon fine, il risultato viene restituito subito con return result. Se invece l’operazione lancia un errore, il blocco catch intercetta il problema.
Se non abbiamo ancora esaurito i tentativi, aspettiamo con await delay(delayMs) e poi aumentiamo il tempo di attesa con delayMs *= factor. Questo è il backoff esponenziale: 300ms, poi 600ms, poi 1200ms, e così via.
Se invece siamo all’ultimo tentativo, rilanciamo un errore più chiaro con un messaggio che spiega quante prove sono state fatte e qual è stata la causa originale.
3. La funzione fetchUserProfile
Questa funzione simula una chiamata a un servizio esterno. In un caso reale potrebbe essere una fetch verso un endpoint REST, una query a un database remoto o una richiesta a un microservizio. Qui generiamo un errore casuale per mostrare il comportamento del retry.
Il vantaggio di questo approccio è che retryWithBackoff non dipende dal tipo di operazione: possiamo riutilizzarlo con una chiamata HTTP, con l’accesso a un file, con un job remoto o con qualunque funzione che restituisca una Promise.
Best practice
- Ritenta solo gli errori temporanei: non ha senso riprovare se l’errore è causato da dati invalidi o da un bug di logica. Il retry è utile soprattutto per problemi di rete, timeout o servizi instabili.
- Usa un numero limitato di tentativi: ritentare all’infinito può peggiorare le prestazioni e bloccare il flusso dell’applicazione.
- Preferisci il backoff esponenziale: evitare retry troppo ravvicinati riduce il carico sul servizio e aumenta le probabilità di successo quando il problema è temporaneo.
- Logga i tentativi con criterio: nei progetti reali è utile sapere quante volte un’operazione è stata riprovata, ma senza riempire i log di rumore inutile.
- Separa la logica di retry dalla logica di business: la funzione che fa il retry dovrebbe essere generica, così puoi riutilizzarla in più punti del progetto.
- Non nascondere gli errori: se tutti i tentativi falliscono, rilancia un errore chiaro. L’applicazione deve sapere che l’operazione non è riuscita.
Riepilogo
Il pattern retry con backoff è una tecnica molto pratica per rendere il codice asincrono più robusto e leggibile. Con async/await possiamo implementarlo in modo lineare, evitando catene di Promise difficili da seguire.
Abbiamo visto come:
- creare una funzione delay per introdurre pause asincrone;
- costruire una funzione generica retryWithBackoff;
- gestire gli errori in modo chiaro e controllato;
- riutilizzare lo stesso approccio per operazioni diverse.
Questo pattern è particolarmente utile quando lavori con API esterne, servizi cloud, integrazioni di terze parti o qualunque sistema soggetto a errori temporanei. In pratica, è uno strumento semplice ma molto efficace per scrivere codice asincrono più professionale.
Approfondisci con risorse ufficiali
- MDN Web Docs - async function: documentazione ufficiale sulle funzioni asincrone in JavaScript.
- MDN Web Docs - Promise: approfondimento completo sul modello delle Promise.
- MDN Web Docs - setTimeout: utile per capire come funziona la base del delay asincrono.
- JavaScript.info - Promises, async/await: guida molto chiara e pratica sul tema.
- Node.js Documentation: utile se vuoi applicare retry e backoff in ambienti server-side.
