JavaScript Async/Await: rendere leggibile il codice asincrono con il pattern “timeout e cancellazione”

by Anastasia P.
SHARE
JavaScript Async/Await: rendere leggibile il codice asincrono con il pattern “timeout e cancellazione”
© Guida-HTML5.it

Introduzione

Quando si lavora con Async/Await, uno dei problemi più comuni non è solo eseguire operazioni asincrone, ma farlo in modo leggibile, controllabile e robusto. In molti progetti reali, infatti, non basta attendere il risultato di una Promise: bisogna anche gestire casi come una richiesta troppo lenta, un’azione annullata dall’utente o un’operazione che non deve più proseguire perché il contesto è cambiato.

Questo tutorial si concentra su un sotto-argomento molto pratico: come scrivere codice asincrono leggibile usando Async/Await insieme a timeout e cancellazione. È un tema utile in applicazioni web, dashboard, e-commerce, form complessi e interfacce dove l’utente può cambiare idea mentre una richiesta è ancora in corso.

L’obiettivo è mostrare come strutturare il codice in modo chiaro, evitando catene di Promise difficili da seguire e riducendo bug legati a richieste “vecchie” che aggiornano la UI nel momento sbagliato.

Codice completo

// Simula una chiamata asincrona lenta, ad esempio verso un´API
function fetchUserProfile(userId, signal) {
  return new Promise((resolve, reject) => {
    const delay = 2500;

    const timer = setTimeout(() => {
      resolve({
        id: userId,
        name: "Luca Rossi",
        role: "Frontend Developer"
      });
    }, delay);

    // Se arriva una cancellazione, interrompiamo il lavoro
    signal.addEventListener("abort", () => {
      clearTimeout(timer);
      reject(new Error("Operazione annullata"));
    });
  });
}

// Restituisce una Promise che fallisce se il tempo massimo viene superato
function withTimeout(promise, ms) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error(`Timeout: superati ${ms}ms`));
    }, ms);

    promise
      .then((value) => {
        clearTimeout(timer);
        resolve(value);
      })
      .catch((error) => {
        clearTimeout(timer);
        reject(error);
      });
  });
}

// Funzione principale: usa Async/Await in modo leggibile
async function loadProfile(userId) {
  const controller = new AbortController();

  try {
    console.log("Avvio caricamento profilo...");

    // Simuliamo un´azione che potrebbe essere annullata
    const profilePromise = fetchUserProfile(userId, controller.signal);

    // Impostiamo un timeout massimo di attesa
    const profile = await withTimeout(profilePromise, 2000);

    console.log("Profilo caricato:", profile);
    return profile;
  } catch (error) {
    console.error("Errore durante il caricamento:", error.message);
    throw error;
  } finally {
    // Se il caricamento non è andato a buon fine, possiamo annullare il lavoro residuo
    controller.abort();
    console.log("Pulizia completata");
  }
}

// Esempio di utilizzo
loadProfile(42)
  .then((profile) => {
    console.log("Risultato finale:", profile);
  })
  .catch((error) => {
    console.log("Fallimento finale:", error.message);
  });

Spiegazione

Il codice sopra unisce tre idee fondamentali:

  • Async/Await per scrivere il flusso in modo lineare e comprensibile.
  • Timeout per evitare di attendere all’infinito una risposta lenta.
  • Cancellazione per interrompere un’operazione non più necessaria.

1. La funzione fetchUserProfile

Questa funzione simula una richiesta asincrona. In un caso reale potrebbe essere una chiamata a fetch() o a un servizio esterno. Qui usiamo setTimeout per rappresentare una risposta lenta.

Il punto importante è l’uso di AbortController attraverso il parametro signal. Se il segnale viene annullato, il timer viene cancellato e la Promise viene rifiutata. Questo è molto utile quando l’utente cambia pagina o avvia una nuova richiesta prima che la precedente sia conclusa.

2. La funzione withTimeout

Questa utility incapsula il concetto di timeout. Riceve una Promise e un limite massimo in millisecondi. Se la Promise si risolve prima del tempo, il timer viene annullato e il valore viene restituito. Se invece il tempo scade, viene generato un errore.

Separare questa logica in una funzione dedicata è una buona scelta di leggibilità: evita di ripetere la stessa struttura in più punti del progetto.

3. La funzione loadProfile

Qui vediamo il vero vantaggio di Async/Await: il codice si legge dall’alto verso il basso, come un flusso naturale.

  • creiamo un AbortController;
  • avviamo la richiesta;
  • attendiamo il risultato con await;
  • gestiamo eventuali errori con try/catch;
  • nel blocco finally eseguiamo la pulizia finale.

Il blocco finally è particolarmente utile perché viene eseguito sia in caso di successo sia in caso di errore. In un’app reale, qui potresti nascondere uno spinner, liberare risorse o interrompere un’operazione residua.

4. Perché questo approccio migliora la leggibilità

Spesso il codice asincrono diventa difficile da mantenere quando contiene molte condizioni, timer, richieste e aggiornamenti della UI. Con Async/Await puoi dare una forma più chiara al flusso, ma il vero salto di qualità arriva quando separi le responsabilità:

  • una funzione gestisce la richiesta;
  • una funzione gestisce il timeout;
  • una funzione coordina il tutto.

Questo rende il codice più facile da testare, più semplice da leggere e meno soggetto a errori.

Best practice

  • Usa Async/Await per il flusso principale: evita di mescolare troppi .then() con await nello stesso blocco, perché riduce la chiarezza.
  • Estrai le utility riutilizzabili: funzioni come withTimeout dovrebbero vivere in moduli dedicati, così puoi riutilizzarle in più contesti.
  • Gestisci la cancellazione: quando l’utente può cambiare schermata o avviare una nuova richiesta, usa AbortController per evitare aggiornamenti obsoleti.
  • Usa try/catch/finally con criterio: il blocco try dovrebbe contenere solo la parte asincrona davvero necessaria, per mantenere il controllo del flusso semplice.
  • Definisci timeout realistici: un timeout troppo breve genera falsi errori, uno troppo lungo rende l’interfaccia lenta a reagire.
  • Logga gli errori in modo utile: mostra messaggi comprensibili agli sviluppatori, ma anche feedback chiari all’utente finale.

Riepilogo

Async/Await non serve solo a “scrivere meno codice”: serve soprattutto a scrivere codice asincrono più leggibile e controllabile. In questo tutorial abbiamo visto come combinare Async/Await con timeout e cancellazione per gestire scenari molto comuni nelle applicazioni reali.

Il vantaggio principale è la chiarezza: il flusso è lineare, le responsabilità sono separate e il comportamento dell’app è più prevedibile. Quando una richiesta impiega troppo tempo o non è più utile, puoi interromperla in modo pulito e mantenere la UI coerente.

Se vuoi migliorare la qualità del tuo codice asincrono, questo è uno dei pattern più utili da imparare dopo le basi di Async/Await.

Approfondisci con risorse ufficiali

  • MDN - Async function: documentazione ufficiale sulle funzioni async e sul comportamento di await.
  • MDN - Promise: per ripassare il funzionamento delle Promise e capire meglio il legame con Async/Await.
  • MDN - AbortController: guida ufficiale alla cancellazione delle operazioni asincrone.
  • MDN - setTimeout: utile per comprendere la base del timeout nel JavaScript del browser.
  • WHATWG Fetch Standard: riferimento tecnico per chi vuole approfondire fetch e la gestione delle richieste annullabili.

SHARE