JavaScript Async/Await: rendere il codice asincrono più leggibile con il pattern “retry con backoff”

by Anastasia P.
SHARE
JavaScript Async/Await: rendere il codice asincrono più leggibile con il pattern “retry con backoff”
© Guida-HTML5.it

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.

SHARE