// apis · Web Platform Advent #20
AbortController : annuler des fetch, des délais et des écouteurs d'événements
Un seul contrôleur annule un fetch, un délai d'attente et toute une série d'écouteurs d'événements. Comment fonctionnent AbortController et AbortSignal, pourquoi une annulation manuelle et un timeout ne lèvent pas la même erreur, et l'astuce de nettoyage que presque tout le monde ignore.
Démarrer une opération asynchrone est facile. Arrêter celle dont vous n'avez plus besoin est la partie que la plupart du code saute, et c'est de là que viennent les fuites de mémoire, les situations de compétition et les résultats périmés. AbortController est la réponse unique du navigateur à tout cela.
Un contrôleur, un signal
MDN le définit sans détour : l'interface AbortController représente un objet contrôleur qui vous permet d'interrompre une ou plusieurs requêtes Web au moment voulu. La forme est toujours la même. Vous créez un contrôleur, vous lisez son signal, et vous confiez ce signal à l'opération que vous démarrez.
La propriété signal renvoie une instance d'objet AbortSignal, qui peut être utilisée pour communiquer avec une opération asynchrone, ou pour l'interrompre. Le contrôleur est ce que vous gardez ; le signal est ce que vous donnez.
const controller = new AbortController();
const res = await fetch('/api/articles', { signal: controller.signal });
// ailleurs, plus tard :
controller.abort(); Notez le sens de la relation. Le code qui travaille ne décide jamais de s'arrêter ; il accepte seulement d'écouter. C'est cette séparation qui permet à un seul appelant d'annuler plusieurs opérations d'un coup.
Les deux erreurs, et pourquoi cela compte
Voici la partie qui mérite une lecture attentive, car la même annulation peut lever deux choses différentes.
Quand vous appelez vous-même abort(), la promesse de fetch() est rejetée avec une DOMException nommée AbortError. Quand le signal provient de AbortSignal.timeout(), la promesse de fetch() est rejetée avec une DOMException TimeoutError à la place.
Le test habituel est donc incomplet :
try {
const res = await fetch(url, { signal });
} catch (err) {
if (err.name === 'AbortError') {
// l'utilisateur ou notre propre code a annulé
} else if (err.name === 'TimeoutError') {
// le délai est dépassé - une situation totalement différente
} else {
throw err; // un vrai échec réseau ou d'analyse
}
} Confondre les deux fait perdre une information réelle. Un utilisateur qui quitte la page n'est pas un problème ; une requête qui a dépassé son délai en est probablement un, et mérite d'être signalée ou réessayée. Un code qui ne cherche que AbortError traite chaque timeout comme un non-événement silencieux.
Des délais d'attente sans la tuyauterie
Le schéma que la plupart des gens écrivent encore associe un contrôleur à un setTimeout et à un clearTimeout dans un bloc finally. Cela fonctionne, et ce n'est plus nécessaire.
AbortSignal.timeout() renvoie une instance d'AbortSignal qui s'interrompra automatiquement au bout d'un temps donné. Pas de contrôleur, pas de minuteur à nettoyer, rien qui puisse fuir :
// Avant : trois pièces mobiles
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}
// Maintenant : une seule
const res = await fetch(url, { signal: AbortSignal.timeout(5000) }); Surveillez le nom de l'erreur quand vous faites cette bascule. L'ancienne version levait AbortError ; la nouvelle lève TimeoutError, donc tout bloc catch déjà en place devra être mis à jour.
L'astuce que presque tout le monde ignore : les écouteurs d'événements
AbortSignal n'est pas une fonctionnalité de fetch. addEventListener accepte un signal dans ses options, et quand ce signal s'interrompt, l'écouteur est retiré pour vous. Cela remplace discrètement le code de nettoyage le plus fastidieux du travail front-end.
Comparez avec le retrait manuel des écouteurs, qui impose de conserver une référence vers chaque fonction passée :
// Avant : vous devez garder chaque gestionnaire pour le retirer
function onScroll() { /* ... */ }
function onResize() { /* ... */ }
window.addEventListener('scroll', onScroll);
window.addEventListener('resize', onResize);
function teardown() {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onResize);
} Avec un signal, les gestionnaires peuvent redevenir des fonctions en ligne, et un seul appel les retire tous :
const controller = new AbortController();
const { signal } = controller;
window.addEventListener('scroll', () => { /* ... */ }, { signal });
window.addEventListener('resize', () => { /* ... */ }, { signal });
document.addEventListener('keydown', () => { /* ... */ }, { signal });
// un seul appel retire les trois
controller.abort(); Le même contrôleur peut aussi détenir le signal d'un fetch en cours. Démonter une vue se réduit alors à un unique abort() qui annule la requête et détache tous les écouteurs d'un même geste.
Combiner des signaux et lire leur état
AbortSignal.any() renvoie un AbortSignal qui s'interrompt dès que l'un des signaux d'interruption fournis s'interrompt. C'est ainsi que vous obtenez un délai limite et une annulation manuelle sur la même opération :
const controller = new AbortController();
const signal = AbortSignal.any([
controller.signal, // l'utilisateur a cliqué sur Annuler
AbortSignal.timeout(10000), // ou dix secondes se sont écoulées
]);
const res = await fetch(url, { signal }); Un signal vous dit aussi où il en est. aborted est un booléen qui indique si la ou les requêtes avec lesquelles le signal communique sont interrompues, et reason est une valeur JavaScript qui fournit la raison de l'interruption, une fois que le signal s'est interrompu. Passer votre propre raison à abort() est le moyen de distinguer les causes plus tard :
controller.abort(new Error('User navigated away'));
// plus tard
if (signal.aborted) console.log(signal.reason.message); Il existe également un événement abort, déclenché lorsque les opérations asynchrones avec lesquelles le signal communique sont interrompues, qui permet à votre propre code de longue durée de coopérer, et pas seulement aux API natives.
Rendre votre propre fonction annulable
Tout ce qui accepte un signal rejoint le même système. Le schéma consiste à vérifier aborted avant de démarrer et à écouter l'événement abort pendant l'exécution :
function wait(ms, { signal } = {}) {
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(signal.reason);
const id = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(id);
reject(signal.reason);
}, { once: true });
});
} Votre propre utilitaire peut désormais être annulé par le contrôleur qui annule déjà vos fetch, ce qui est tout l'intérêt d'avoir un seul mécanisme plutôt que plusieurs.
Aide-mémoire
| Objectif | Appel |
|---|---|
| Annuler manuellement | controller.abort(), lève AbortError |
| Ajouter un délai limite | AbortSignal.timeout(ms), lève TimeoutError |
| L'un ou l'autre des deux | AbortSignal.any([a, b]) |
| Retirer un écouteur automatiquement | addEventListener(type, fn, { signal }) |
| Vérifier l'état | signal.aborted |
| Savoir pourquoi | signal.reason |
| Réagir à l'annulation | l'événement abort |
AbortController est une petite API qui résout discrètement toute une catégorie de problèmes de nettoyage. Confiez un seul signal à vos requêtes, à vos écouteurs et à vos propres utilitaires asynchrones, puis arrêtez-les tous d'un unique appel. N'oubliez simplement pas quelle erreur vous interceptez, car une annulation manuelle et un délai dépassé ne sont pas le même événement.
Pour le versant requêtes, voyez notre guide de l'API Fetch en JavaScript.
Questions fréquentes
- À quoi sert AbortController ?
- MDN le décrit comme un objet contrôleur qui vous permet d'interrompre une ou plusieurs requêtes Web au moment voulu. Vous créez un contrôleur, vous confiez son signal à l'opération que vous démarrez, et vous appelez abort() quand vous voulez l'arrêter. Ce n'est pas réservé à fetch : tout ce qui accepte un AbortSignal peut être annulé de la même façon.
- Quelle erreur lève un fetch annulé ?
- Cela dépend de la manière dont il a été annulé, et c'est là que beaucoup se font piéger. Quand vous appelez vous-même AbortController.abort(), la promesse du fetch est rejetée avec une DOMException nommée AbortError. Quand le signal provient de AbortSignal.timeout(), la promesse est rejetée avec une DOMException TimeoutError à la place. Un code qui ne teste que AbortError laissera silencieusement passer le cas du timeout.
- Comment ajouter un délai d'attente à un fetch ?
- AbortSignal.timeout() renvoie une instance d'AbortSignal qui s'interrompra automatiquement au bout d'un temps donné : vous passez AbortSignal.timeout(5000) comme signal, sans contrôleur, sans setTimeout et sans clearTimeout. N'oubliez pas qu'il rejette avec TimeoutError, et non avec AbortError.
- Un seul contrôleur peut-il annuler plusieurs choses à la fois ?
- Oui, et c'est tout l'intérêt. Un signal peut être confié à un nombre quelconque d'opérations, si bien qu'un unique appel à abort() arrête tous les fetch et retire tous les écouteurs d'événements qui l'ont reçu. C'est le nettoyage le plus propre disponible au moment de démonter une vue.
- Puis-je combiner un délai d'attente et une annulation manuelle ?
- AbortSignal.any() renvoie un AbortSignal qui s'interrompt dès que l'un des signaux d'interruption fournis s'interrompt. Passez-lui le signal de votre contrôleur et un signal de timeout, et l'opération s'arrête au premier des deux qui survient.