10 - AbortController, annuler proprement
Ce que tu vas apprendre
- Comment fonctionne le duo AbortController / AbortSignal
- Annuler des fetch, des event listeners, et des opérations custom
AbortSignal.timeout()etAbortSignal.any()- Le pattern de cleanup dans React useEffect
Prerequisites
Avoir lu l'article 09 - Les combinateurs de Promises.
Pendant longtemps, JavaScript n'avait aucun mecanisme standard pour annuler une opération asynchrone. Tu lancais un fetch et tu attendais. Si l'utilisateur changeait de page, le fetch continuait en arriere-plan, la réponse arrivait, le callback essayait de mettre à jour un composant demonte, et React t'affichait un warning desagreable. AbortController a change ca.
Le principe
AbortController est un objet avec deux propriétés :
signal: unAbortSignalque tu passes aux opérations que tu veux pouvoir annulerabort(): une méthode qui déclenché l'annulation
typescriptconst controller = new AbortController();
// Passe le signal a fetch
fetch("/api/data", { signal: controller.signal })
.then((r) => r.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("Fetch annule");
} else {
throw error;
}
});
// Plus tard, annule le fetch
controller.abort();
Quand tu appelles abort(), toutes les opérations liees au signal sont annulees. Le fetch rejette avec une erreur de type AbortError. C'est une erreur "normale" -- l'utilisateur a demande l'annulation, pas besoin de la logger comme un bug.
AbortSignal.timeout()
Plutot que de bricoler un timeout avec Promise.race (comme dans l'article 09), tu peux utiliser AbortSignal.timeout() :
typescript// Annule automatiquement apres 5 secondes
const response = await fetch("/api/slow", {
signal: AbortSignal.timeout(5000),
});
C'est plus propre que le pattern maison, et ca nettoie le timer automatiquement si le fetch se termine avant le timeout. Pas de fuite mémoire.
La différence avec le pattern Promise.race : ici, le fetch est vraiment annule au niveau réseau. Avec race, le fetch continuait en arriere-plan.
AbortSignal.any()
Parfois tu veux annuler quand n'importe laquelle de plusieurs conditions est remplie. AbortSignal.any() combine plusieurs signaux :
typescriptconst userController = new AbortController();
// Annule si l'utilisateur clique "Annuler" OU si ca prend plus de 10s
const signal = AbortSignal.any([
userController.signal,
AbortSignal.timeout(10_000),
]);
const response = await fetch("/api/heavy-report", { signal });
// L'utilisateur clique "Annuler"
cancelButton.addEventListener("click", () => userController.abort());
Un seul signal qui reagit a deux sources d'annulation. Avant AbortSignal.any(), il fallait écoûter manuellement chaque signal et propager l'abort.
Annuler des event listeners
AbortController ne sert pas qu'a fetch. Tu peux l'utiliser pour nettoyer des event listeners :
typescriptconst controller = new AbortController();
document.addEventListener("mousemove", handleMouseMove, {
signal: controller.signal,
});
document.addEventListener("keydown", handleKeyDown, {
signal: controller.signal,
});
window.addEventListener("resize", handleResize, {
signal: controller.signal,
});
// Un seul appel pour tout nettoyer
controller.abort();
// Equivalent a trois removeEventListener, sans garder les references aux handlers
C'est tellement plus propre que de stocker chaque handler dans une variable pour pouvoir appeler removeEventListener plus tard. Un seul controller, un seul abort(), tout est nettoye.
Opérations async custom annulables
Tu peux rendre tes propres fonctions annulables en acceptant un signal :
typescriptasync function pollStatus(
url: string,
intervalMs: number,
signal?: AbortSignal
): Promise<string> {
while (true) {
signal?.throwIfAborted(); // lance AbortError si deja annule
const response = await fetch(url, { signal });
const data = await response.json();
if (data.status === "complete") {
return data.result;
}
// Attente annulable
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(resolve, intervalMs);
signal?.addEventListener("abort", () => {
clearTimeout(timer);
reject(signal.reason);
}, { once: true });
});
}
}
// Usage
const controller = new AbortController();
const result = await pollStatus("/api/job/123", 2000, controller.signal);
// Annuler le polling
controller.abort();
Le pattern clé : signal.throwIfAborted() au début de chaque itération, et le signal passe a fetch pour annuler aussi la requête en cours. L'attente entre les polls est elle aussi annulable grace au listener sur le signal.
React useEffect et AbortController
Le cas d'usage le plus frequent en frontend. Sur paltemps.fr, chaque composant qui fetch des donnees suit ce pattern :
typescriptfunction PlaceDetail({ placeId }: { placeId: string }) {
const [place, setPlace] = useState<Place | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
async function loadPlace() {
try {
const response = await fetch(`/api/places/${placeId}`, {
signal: controller.signal,
});
const data = await response.json();
setPlace(data);
} catch (err) {
if (err instanceof Error && err.name === "AbortError") {
return; // composant demonte, on ignore
}
setError("Impossible de charger le lieu");
}
}
loadPlace();
return () => controller.abort(); // cleanup
}, [placeId]);
// ...
}
Quand placeId change, React exécuté le cleanup de l'effet précédent (qui appelle abort()), puis lance le nouvel effet. Le fetch de l'ancien ID est annule, pas de race condition, pas de warning "setState on unmounted component".
Verifier si un signal est deja annule
typescriptasync function doWork(signal?: AbortSignal) {
// Methode 1 : verifier la propriete
if (signal?.aborted) {
throw new Error("Already aborted");
}
// Methode 2 : lancer directement (ES2022)
signal?.throwIfAborted();
// Methode 3 : ecouter l'evenement
signal?.addEventListener("abort", () => {
cleanup();
});
}
throwIfAborted() est la méthode la plus concise. Elle lance signal.reason (par défaut un DOMException de type AbortError).
L'erreur classique : réutiliser un controller
typescriptconst controller = new AbortController();
controller.abort();
// Ce fetch est annule IMMEDIATEMENT
await fetch("/api/data", { signal: controller.signal });
// AbortError !
Un AbortController est a usage unique. Une fois abort() appele, le signal reste dans l'état "aborted" indefiniment. Cree un nouveau controller pour chaque opération ou groupe d'opérations.
Résumé
AbortControllerfournit un mecanisme standard d'annulationAbortSignal.timeout()remplace le patternPromise.racepour les timeoutsAbortSignal.any()combine plusieurs conditions d'annulation- Passe le signal a
fetch,addEventListener, et tes fonctions custom - Dans React, créé un controller dans
useEffectet appelleabort()dans le cleanup - Un controller est a usage unique, n'essaie pas de le réutiliser
Article précédent : 09 - Les combinateurs de Promises Article suivant : 11 - Race conditions en JavaScript