
AbortController: come annullare una fetch in JavaScript
Come usare AbortController e AbortSignal per annullare richieste fetch e altre operazioni asincrone, evitando race condition e sprechi di rete.
Una casella di ricerca con autocomplete che chiama un’API a ogni carattere digitato è l’esempio da manuale, ma il problema è più generale di così: qualunque fetch che puoi lanciare più volte prima che la precedente sia finita — un filtro che cambia, una tab che l’utente riapre, un componente che si smonta mentre la richiesta è ancora in volo — ha lo stesso bug latente. Le risposte possono arrivare in un ordine diverso da quello in cui sono partite, e se aggiorni lo stato con l’ultima risposta ricevuta invece che con l’ultima richiesta, l’utente vede per un istante il risultato sbagliato.
AbortController esiste esattamente per questo: dà a JavaScript un modo standard per dire a un’operazione asincrona già avviata “non mi interessa più il tuo risultato, fermati”.
Il problema, concretamente
async function cercaProdotti(query) {
const res = await fetch(`/api/search?q=${query}`);
const data = await res.json();
renderRisultati(data); // quale richiesta vince?
}
input.addEventListener('input', (e) => cercaProdotti(e.target.value));
Digita “mel”, poi subito dopo “mela”. Partono due fetch. Se la richiesta per “mel” — magari perché il server è momentaneamente più lento su quella query specifica — risponde dopo quella per “mela”, renderRisultati viene chiamata due volte e l’ultima chiamata vince: sullo schermo restano i risultati di “mel”, anche se l’utente ha già digitato “mela” da un pezzo. Non è un errore che lanci un’eccezione, è un bug puramente visivo, silenzioso, difficile da riprodurre in modo affidabile — il tipo di bug che finisce ignorato per mesi perché “a volte capita, non si sa perché”.
La soluzione: un controller per richiesta
let controllerAttuale = null;
async function cercaProdotti(query) {
controllerAttuale?.abort(); // annulla la richiesta precedente, se c'è
controllerAttuale = new AbortController();
try {
const res = await fetch(`/api/search?q=${query}`, {
signal: controllerAttuale.signal,
});
const data = await res.json();
renderRisultati(data);
} catch (err) {
if (err.name === 'AbortError') return; // annullata di proposito, non un errore reale
console.error('Ricerca fallita', err);
}
}
Ogni nuova chiamata annulla il controller della chiamata precedente prima di crearne uno nuovo. Quando abort() viene chiamato, fetch rigetta la sua promise con un AbortError — per questo il blocco catch lo riconosce esplicitamente e semplicemente esce, senza trattarlo come un errore di rete vero.
⚠ Attenzione
fetch() rigetta, non risolve, quando viene annullata: un errore comune è dimenticare che AbortError passa dal catch, non da un controllo dentro il blocco try. Se il tuo codice non distingue AbortError dagli altri errori di rete, finisci per mostrare un messaggio “Errore di connessione” all’utente ogni volta che semplicemente digita un carattere in più — un bug fastidioso quanto quello che stavi cercando di risolvere.
Non solo fetch: qualunque API che accetta un signal
AbortSignal non è specifico di fetch — è diventato un pattern standard che molte API web nativas accettano:
// Un event listener che si rimuove da solo
document.addEventListener('click', handler, { signal: controller.signal });
// Un timer annullabile senza salvare l'id con clearTimeout
setTimeout(() => console.log('eseguito'), 1000, /* non nativamente, serve un wrapper */);
Per setTimeout non c’è supporto nativo diretto al signal, ma il pattern si integra facilmente:
function attendiCancellabile(ms, signal) {
return new Promise((resolve, reject) => {
const id = setTimeout(resolve, ms);
signal.addEventListener('abort', () => {
clearTimeout(id);
reject(new DOMException('Annullato', 'AbortError'));
});
});
}
Lo stesso AbortController può quindi annullare più operazioni contemporaneamente collegate allo stesso signal — utile quando un singolo evento (l’utente naviga via, un componente si smonta) deve fermare più cose in una volta sola, senza tenere traccia manualmente di ognuna.
Cancellazione automatica con un timeout
Un uso pratico che vale la pena conoscere: AbortSignal.timeout() crea un signal che si annulla da solo dopo N millisecondi, utile per non lasciare mai una fetch appesa indefinitamente su una rete lenta o un server che non risponde:
const res = await fetch('/api/dati-lenti', {
signal: AbortSignal.timeout(5000), // annulla automaticamente dopo 5 secondi
});
Puoi anche combinare più signal con AbortSignal.any() — la richiesta si annulla non appena uno qualsiasi dei signal forniti scatta, utile per unire “l’utente ha annullato manualmente” e “è scaduto il timeout” nella stessa fetch senza gestirli separatamente:
const signal = AbortSignal.any([controllerUtente.signal, AbortSignal.timeout(5000)]);
Il legame con debounce e throttle
Se il problema che stai risolvendo è “l’utente digita troppo velocemente e genero troppe richieste”, AbortController e il debounce non sono alternative, sono complementari e risolvono due problemi diversi: il debounce riduce quante richieste partono nel tempo (aspetta una pausa nella digitazione prima di chiamare l’API); AbortController garantisce che, delle richieste che partono comunque, solo l’ultima conti davvero. Un campo di ricerca fatto bene, in pratica, quasi sempre li usa insieme.
💡 Consiglio
Non serve solo per fetch di rete: il pattern vale ogni volta che hai un’operazione asincrona il cui risultato può diventare irrilevante prima di completarsi — inclusi casi non di rete, come un IntersectionObserver collegato a un componente che si smonta, o una chiamata stopPropagation/preventDefault su un evento che potrebbe non arrivare mai se l’elemento viene rimosso prima. Se già gestisci la propagazione degli eventi con attenzione, AbortSignal è lo stesso tipo di disciplina applicata al codice asincrono invece che agli eventi del DOM.