Un utilisateur tape trois lettres dans un champ de recherche, puis efface tout. Trois requêtes sont parties, et la plus lente peut répondre en dernier, écrasant l'affichage avec un résultat périmé.
Attendre poliment ne suffit pas : il faut dire à une opération en cours de s'arrêter. C'est le rôle d'AbortController.
Définition
Un AbortController est un objet qui expose deux choses : une propriété signal, transmise à l'opération à surveiller, et une méthode abort(), qui déclenche l'annulation. Le signal est le lien entre les deux.
const controleur = new AbortController();
const signal = controleur.signal;
console.log(signal.aborted); // false
signal.addEventListener("abort", () => {
console.log("annulé :", signal.reason.name); // annulé : AbortError
});
controleur.abort();
console.log(signal.aborted); // trueL'annulation est définitive : un signal déclenché ne revient pas en arrière, et un nouvel appel demande un nouveau contrôleur.
Annuler une requête
fetch() accepte le signal dans ses options. Une fois abort() appelé, la requête est interrompue et la promesse est rompue avec une erreur nommée AbortError.
async function chercher(terme, signal) {
try {
const reponse = await fetch("/api/recherche?q=" + terme, { signal });
return await reponse.json();
} catch (err) {
if (err.name === "AbortError") return null; // annulation voulue
throw err; // vraie panne
}
}Distinguer les deux cas est indispensable : une annulation volontaire n'est pas un incident et ne doit ni s'afficher ni remonter dans les journaux d'erreurs.
Un signal, plusieurs usages
- Retirer des écouteurs d'un coup.
addEventListener("click", f, { signal })détache l'écouteur automatiquement au premierabort(), quel que soit leur nombre. - Poser une limite de temps.
AbortSignal.timeout(5000)se déclenche seul après cinq secondes, avec une raison nomméeTimeoutError, là où un Promise.race() laisserait l'appel courir. - Combiner des raisons d'arrêter.
AbortSignal.any([a, b])rend un signal déclenché dès que l'un des deux l'est. - Vérifier au bon moment.
signal.throwIfAborted()lance l'erreur si l'annulation a déjà eu lieu, et coupe une boucle longue entre deux étapes.
const controleur = new AbortController();
const signal = AbortSignal.any([controleur.signal, AbortSignal.timeout(5000)]);
// La requête s'arrête au premier des deux : abandon manuel ou cinq secondes
fetch("/api/rapport", { signal });abort() accepte une raison de votre choix : controleur.abort(new Error("recherche remplacée")). Elle se retrouve dans signal.reason et dans l'erreur reçue, ce qui rend les journaux bien plus lisibles qu'un AbortError anonyme.
Questions fréquentes
Peut-on réutiliser un contrôleur après annulation ?
Non, un signal déclenché le reste. Chaque opération annulable demande son propre contrôleur, créé au moment du lancement. Dans un champ de recherche, on garde donc le contrôleur en cours, on l'annule à la frappe suivante et on en crée un nouveau.
La requête est-elle vraiment arrêtée côté serveur ?
La connexion est coupée, mais le serveur peut déjà avoir commencé, voire terminé son travail. L'annulation protège l'interface et libère la connexion, elle n'annule pas un effet déjà produit. Une écriture en base doit rester protégée autrement.
Où placer l'annulation dans une interface à composants ?
Dans le nettoyage de l'effet qui a lancé la requête, appelé au retrait du composant ou avant un nouvel appel. C'est ce qui évite les mises à jour sur un écran disparu, un réflexe travaillé dans la formation React.