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.
