AbortController: cómo cancelar un fetch (y por qué conviene)
Aprendé a cancelar peticiones HTTP con AbortController, evitar condiciones de carrera en la UI y limpiar efectos en React o en código vanilla.
El fetch que sigue vivo cuando ya no lo necesitás
Imaginá que el usuario escribe en un buscador. Cada tecla dispara un fetch al backend. Si escribe rápido, podés tener tres o cuatro peticiones en vuelo al mismo tiempo. La que termina última no siempre es la más reciente: a veces una respuesta vieja pisa el resultado nuevo y la UI muestra datos incorrectos.
Lo mismo pasa si el usuario cambia de página o cierra un modal mientras una petición sigue pendiente. El fetch no se cancela solo. Seguir esperando esa respuesta gasta red, puede actualizar estado de un componente que ya no existe y, en el peor caso, te deja con un bug difícil de reproducir.
Para eso existe AbortController: una API del navegador (y de Node) que te deja señalar “pará” a una operación asíncrona. fetch la entiende de fábrica.
Qué es AbortController
AbortController es un objeto chico con dos piezas:
- Un
signalque le pasás a quien tiene que escuchar la cancelación. - Un método
abort()que marca ese signal como abortado.
Cuando llamás abort(), cualquier fetch asociado a ese signal se rechaza con un error de tipo AbortError (en la práctica, un DOMException). No es un fallo de red: es una cancelación intencional.
const controller = new AbortController();
fetch("/api/buscar?q=zapatillas", { signal: controller.signal })
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => {
if (err.name === "AbortError") {
console.log("La petición se canceló a propósito");
return;
}
throw err;
});
// Más tarde, si ya no necesitás la respuesta:
controller.abort();
El punto clave: un controller, un signal. Si querés cancelar un grupo de peticiones juntas, compartís el mismo signal. Si querés cancelarlas por separado, creás un controller por petición.
El patrón más útil: cancelar la búsqueda anterior
En un input de búsqueda, lo habitual es abortar la petición anterior antes de disparar la nueva:
let controller = null;
async function buscar(query) {
controller?.abort();
controller = new AbortController();
try {
const res = await fetch(`/api/buscar?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (err.name === "AbortError") return null; // se canceló; no es un error real
throw err;
}
}
Así solo la última consulta “gana”. Las anteriores mueren en silencio y no actualizan la lista de resultados.
Timeouts con AbortSignal.timeout
A veces no querés cancelar porque el usuario se fue, sino porque la petición tarda demasiado. En navegadores modernos podés usar AbortSignal.timeout(ms) sin armar el controller a mano:
try {
const res = await fetch("/api/lento", {
signal: AbortSignal.timeout(5000), // 5 segundos
});
const data = await res.json();
} catch (err) {
if (err.name === "TimeoutError" || err.name === "AbortError") {
console.log("Se agotó el tiempo de espera");
} else {
throw err;
}
}
Si necesitás compatibilidad más amplia o combinar timeout con cancelación manual, creás el controller vos y usás setTimeout(() => controller.abort(), 5000).
En React: limpiar el efecto
En un useEffect que dispara un fetch, la función de cleanup es el lugar natural para abortar:
useEffect(() => {
const controller = new AbortController();
async function cargar() {
try {
const res = await fetch(`/api/usuarios/${id}`, {
signal: controller.signal,
});
const data = await res.json();
setUsuario(data);
} catch (err) {
if (err.name === "AbortError") return;
setError(err);
}
}
cargar();
return () => controller.abort();
}, [id]);
Cuando cambia id o el componente se desmonta, React corre el cleanup, abortás el fetch viejo y evitás el clásico warning de “Can’t perform a React state update on an unmounted component” (y, más importante, evitás mostrar datos del id anterior).
Errores comunes
-
Tratar
AbortErrorcomo un fallo de la app
Si mostrás un toast genérico en elcatch, el usuario va a ver errores fantasmas cada vez que cancelás. Filtrá porerr.name === "AbortError"(oerr.name === "TimeoutError") y salí sin alarma. -
Reutilizar un controller ya abortado
Una vez que llamasteabort(), ese signal queda abortado para siempre. Para la siguiente petición creá un nuevoAbortController. -
Olvidar pasar el
signal
Llamarcontroller.abort()sin haber pasadosignalalfetchno cancela nada. El controller solo habla con quien escucha su signal. -
Abortar demasiado tarde
Si abortás después de haber parseado el JSON y justo antes desetState, igual podés pisar estado. Lo ideal es abortar la petición; si además querés ser defensivo, chequeá un flag o ignorá el resultado si el signal ya está abortado.
No solo sirve para fetch
Cualquier API que acepte un AbortSignal puede cancelarse igual: addEventListener (con { signal }), algunas APIs de streams, librerías de HTTP como Axios (vía signal o adaptadores), y cada vez más APIs del ecosistema. La idea es la misma: una señal compartida para decir “esto ya no hace falta”.
Conclusión
AbortController no hace tu código más “moderno” por sí solo: te da control sobre operaciones que, de otra forma, siguen vivas cuando la UI ya cambió de idea. En búsquedas, cambios de ruta y efectos de React, cancelar a tiempo evita condiciones de carrera y trabajo innecesario.
Mi consejo es simple: cada vez que un fetch pueda quedar obsoleto antes de terminar, acompañalo de un AbortController. Empezá por el patrón de “abortá el anterior, creá uno nuevo” en búsquedas y por el cleanup del useEffect. Con eso cubrís la mayoría de los casos reales, y el resto (timeouts, cancelación en lote) encaja encima sin inventar infraestructura rara.
Si querés el detalle de la API, MDN lo documenta bien: AbortController y AbortSignal.
Comentarios