// apis · Web Platform Advent #20
AbortController: cancelar fetch, tiempos de espera y escuchadores de eventos
Un solo controlador cancela un fetch, un tiempo de espera y un montón de escuchadores de eventos. Cómo funcionan AbortController y AbortSignal, por qué una cancelación manual y un timeout lanzan errores distintos, y el truco de limpieza que casi todo el mundo pasa por alto.
Iniciar una operación asíncrona es fácil. Detener una que ya no te importa es la parte que la mayoría del código se salta, y de ahí vienen las fugas de memoria, las condiciones de carrera y los resultados obsoletos. AbortController es la respuesta única del navegador a todo ello.
Un controlador, un signal
MDN lo define con claridad: la interfaz AbortController representa un objeto controlador que te permite abortar una o varias peticiones Web cuando lo desees. La forma es siempre la misma. Creas un controlador, lees su signal y entregas ese signal a lo que estés iniciando.
La propiedad signal devuelve una instancia de objeto AbortSignal, que puede usarse para comunicarse con una operación asíncrona, o para abortarla. El controlador es lo que te quedas; el signal es lo que entregas.
const controller = new AbortController();
const res = await fetch('/api/articles', { signal: controller.signal });
// en otro sitio, más tarde:
controller.abort(); Fíjate en la dirección. El código que hace el trabajo nunca decide detenerse; solo accede a escuchar. Esa separación es lo que permite que un único llamante cancele muchas operaciones a la vez.
Los dos errores, y por qué importa
Esta es la parte que merece leerse con atención, porque la misma cancelación puede lanzar dos cosas distintas.
Cuando llamas tú mismo a abort(), la promesa de fetch() se rechaza con una DOMException llamada AbortError. Cuando el signal venía de AbortSignal.timeout(), la promesa de fetch() se rechaza en su lugar con una DOMException TimeoutError.
Así que la comprobación habitual está incompleta:
try {
const res = await fetch(url, { signal });
} catch (err) {
if (err.name === 'AbortError') {
// el usuario o nuestro propio código lo canceló
} else if (err.name === 'TimeoutError') {
// se agotó el plazo - una situación completamente distinta
} else {
throw err; // un fallo real de red o de análisis
}
} Fundir ambos casos hace perder información real. Que un usuario abandone la página no es un problema; que una petición se quede sin tiempo probablemente sí lo sea, y merece registrarse o reintentarse. El código que solo busca AbortError trata cada tiempo de espera agotado como un no-evento silencioso.
Tiempos de espera sin fontanería
El patrón que la mayoría sigue escribiendo combina un controlador con un setTimeout y un clearTimeout en un bloque finally. Funciona, y ya no es necesario.
AbortSignal.timeout() devuelve una instancia de AbortSignal que abortará automáticamente después de un tiempo determinado. Sin controlador, sin temporizador que limpiar, sin nada que pueda quedar colgando:
// Antes: tres piezas móviles
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}
// Ahora: una
const res = await fetch(url, { signal: AbortSignal.timeout(5000) }); Vigila el nombre del error cuando hagas este cambio. La versión antigua lanzaba AbortError; la nueva lanza TimeoutError, así que cualquier bloque catch que ya tuvieras habrá que actualizarlo.
El truco que casi todo el mundo pasa por alto: los escuchadores de eventos
AbortSignal no es una función de fetch. addEventListener acepta un signal entre sus opciones y, cuando ese signal aborta, el escuchador se elimina por ti. Esto sustituye discretamente el código de limpieza más tedioso del trabajo front-end.
Compáralo con eliminar escuchadores a mano, lo que obliga a conservar una referencia a cada función que has pasado:
// Antes: debes guardar cada manejador para poder eliminarlo
function onScroll() { /* ... */ }
function onResize() { /* ... */ }
window.addEventListener('scroll', onScroll);
window.addEventListener('resize', onResize);
function teardown() {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onResize);
} Con un signal, los manejadores pueden volver a ser funciones en línea, y una sola llamada los elimina todos:
const controller = new AbortController();
const { signal } = controller;
window.addEventListener('scroll', () => { /* ... */ }, { signal });
window.addEventListener('resize', () => { /* ... */ }, { signal });
document.addEventListener('keydown', () => { /* ... */ }, { signal });
// una sola llamada elimina los tres
controller.abort(); El mismo controlador puede además estar sosteniendo el signal de un fetch en curso. Desmontar una vista se convierte entonces en un único abort() que cancela la petición y desvincula todos los escuchadores a la vez.
Combinar señales y leer su estado
AbortSignal.any() devuelve un AbortSignal que aborta cuando aborta cualquiera de las señales de aborto indicadas. Así es como consigues un plazo límite y una cancelación manual sobre la misma operación:
const controller = new AbortController();
const signal = AbortSignal.any([
controller.signal, // el usuario pulsó Cancelar
AbortSignal.timeout(10000), // o pasaron diez segundos
]);
const res = await fetch(url, { signal }); Un signal también te dice en qué punto está. aborted es un booleano que indica si la petición o peticiones con las que el signal se comunica están abortadas, y reason es un valor JavaScript que proporciona el motivo del aborto, una vez que el signal ha abortado. Pasar tu propio motivo a abort() es la manera de distinguir causas más adelante:
controller.abort(new Error('User navigated away'));
// más tarde
if (signal.aborted) console.log(signal.reason.message); También existe un evento abort, que se invoca cuando las operaciones asíncronas con las que el signal se comunica son abortadas, lo que permite que coopere tu propio código de larga duración, y no solo las API integradas.
Hacer que tu propia función sea abortable
Todo lo que acepta un signal entra en el mismo sistema. El patrón consiste en comprobar aborted antes de empezar y escuchar el evento abort mientras se ejecuta:
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 });
});
} Ahora tu propia función auxiliar puede cancelarse con el mismo controlador que cancela tus fetch, que es justamente el sentido de que haya un mecanismo y no varios.
Referencia rápida
| Objetivo | Llamada |
|---|---|
| Cancelar manualmente | controller.abort(), lanza AbortError |
| Añadir un plazo límite | AbortSignal.timeout(ms), lanza TimeoutError |
| Cualquiera de los dos | AbortSignal.any([a, b]) |
| Eliminar un escuchador automáticamente | addEventListener(type, fn, { signal }) |
| Comprobar el estado | signal.aborted |
| Averiguar el motivo | signal.reason |
| Reaccionar a la cancelación | el evento abort |
AbortController es una API pequeña que resuelve discretamente una gran categoría de problemas de limpieza. Entrega un solo signal a tus peticiones, a tus escuchadores y a tus propias funciones asíncronas, y luego detenlos todos con una única llamada. Solo recuerda qué error estás capturando, porque una cancelación manual y un plazo agotado no son el mismo suceso.
Para la parte de las peticiones, consulta nuestra guía de la API Fetch en JavaScript.
Preguntas frecuentes
- ¿Para qué sirve AbortController?
- MDN lo describe como un objeto controlador que te permite abortar una o varias peticiones Web cuando lo desees. Creas un controlador, entregas su signal a lo que hayas iniciado y llamas a abort() cuando quieres detenerlo. No se limita a fetch: cualquier cosa que acepte un AbortSignal puede cancelarse de la misma manera.
- ¿Qué error lanza un fetch abortado?
- Depende de cómo se haya abortado, y ahí es donde mucha gente tropieza. Cuando llamas tú mismo a AbortController.abort(), la promesa del fetch se rechaza con una DOMException llamada AbortError. Cuando el signal venía de AbortSignal.timeout(), la promesa se rechaza en su lugar con una DOMException TimeoutError. El código que solo comprueba AbortError se saltará en silencio el caso del tiempo de espera.
- ¿Cómo añado un tiempo de espera a un fetch?
- AbortSignal.timeout() devuelve una instancia de AbortSignal que abortará automáticamente después de un tiempo determinado, así que pasas AbortSignal.timeout(5000) como signal y no necesitas ni controlador, ni setTimeout, ni clearTimeout. Recuerda que rechaza con TimeoutError, no con AbortError.
- ¿Puede un solo controlador cancelar varias cosas a la vez?
- Sí, y ese es el sentido del nombre. Un signal puede entregarse a cualquier número de operaciones, de modo que una única llamada a abort() detiene todos los fetch y elimina todos los escuchadores de eventos que lo recibieron. Es la limpieza más ordenada disponible cuando se desmonta una vista.
- ¿Puedo combinar un tiempo de espera con una cancelación manual?
- AbortSignal.any() devuelve un AbortSignal que aborta cuando aborta cualquiera de las señales de aborto indicadas. Pásale el signal de tu controlador y un signal de timeout, y la operación se detendrá con lo que ocurra primero.