
Qué es AbortController
AbortController es una API del lenguaje pensada para cancelar operaciones asíncronas. Su funcionamiento es sencillo: el controlador expone una signal que se entrega a cualquier API cancelable; cuando se llama a controller.abort(), la señal pasa a estado abortado y las APIs que la están observando reaccionan abortando su trabajo.
Está disponible en todos los navegadores modernos y en Node.js desde hace varias versiones. Se usa con fetch, setTimeout (Node/Deno), setInterval, addEventListener, streams, bases de datos y muchas librerías que siguen el mismo patrón.
const controller = new AbortController();
const { signal } = controller;
console.log(signal.aborted); // false
controller.abort();
console.log(signal.aborted); // true
console.log(signal.reason); // DOMException "AbortError" por defecto
abort() admite un argumento opcional que se convierte en signal.reason: así quien reciba la señal puede saber por qué se ha abortado.
Cancelar un fetch
El caso de uso más conocido es cancelar una petición HTTP en curso:
const controller = new AbortController();
try {
const res = await fetch("/api/large-file", { signal: controller.signal });
const data = await res.arrayBuffer();
console.log("Descargado:", data.byteLength, "bytes");
} catch (err) {
if (err.name === "AbortError") {
console.log("Descarga cancelada por el usuario");
} else {
throw err;
}
}
// En otro punto del programa:
// controller.abort();
Puntos clave:
- El error que produce
fetchal abortarse no es unErrorcualquiera: es unDOMExceptionconname === "AbortError". Siempre hay que distinguirlo para no tratar la cancelación como un fallo genuino. - Una vez abortada, no se puede reutilizar la misma señal; hay que crear un controlador nuevo para la siguiente petición.
Reemplazar la petición anterior
Un patrón habitual en búsquedas incrementales: cada vez que el usuario teclea, cancelamos la petición previa y lanzamos una nueva.
let currentController = null;
async function search(query) {
currentController?.abort(); // cancelar la anterior
currentController = new AbortController();
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: currentController.signal
});
return await res.json();
} catch (err) {
if (err.name === "AbortError") return null; // se descartó, no es fallo
throw err;
}
}
Así evitamos condiciones de carrera: nunca renderizamos resultados viejos encima de nuevos.
Timeouts con AbortController
Antes, implementar un timeout con fetch requería una carrera manual entre una promesa y un setTimeout. Con AbortController es mucho más limpio:
function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(new Error(`Timeout ${ms}ms`)), ms);
return fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timer));
}
try {
const res = await fetchWithTimeout("/api/slow", 2000);
console.log(await res.json());
} catch (err) {
if (err.name === "AbortError") {
console.warn("La petición tardó demasiado");
} else {
throw err;
}
}
Al pasar un Error personalizado a abort(), ese objeto se convierte en signal.reason, lo que facilita distinguir timeouts de cancelaciones manuales.
AbortSignal.timeout(ms)
Desde hace poco, existe un atajo estándar que hace exactamente lo mismo: AbortSignal.timeout(ms) devuelve una señal ya configurada para abortarse automáticamente al cabo de ms milisegundos. Evita crear el controlador y el setTimeout a mano:
async function getQuickly(url) {
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
return res.json();
}
Es la forma más ergonómica cuando solo necesitas el timeout y no vas a cancelar manualmente.
Cancelación manual desde la UI
Para cancelar con un botón u otro evento, la señal se comparte entre la operación y el handler que decide abortar:
const cancelBtn = document.getElementById("cancel");
const controller = new AbortController();
cancelBtn.addEventListener("click", () => controller.abort());
try {
const data = await fetch("/api/slow", { signal: controller.signal }).then(r => r.json());
render(data);
} catch (err) {
if (err.name === "AbortError") {
toast("Operación cancelada");
}
}
Si además quieres que el botón escuche solo mientras la operación esté en marcha, puedes añadir el propio listener con signal, como veremos a continuación.
addEventListener con signal
Una utilidad muy cómoda es pasar signal a addEventListener. El listener se elimina automáticamente cuando la señal se aborta, sin necesidad de llamar a removeEventListener:
const controller = new AbortController();
window.addEventListener("scroll", onScroll, { signal: controller.signal });
window.addEventListener("resize", onResize, { signal: controller.signal });
window.addEventListener("keydown", onKeyDown, { signal: controller.signal });
// Cuando terminamos (por ejemplo al desmontar un componente):
controller.abort();
Este patrón es una alternativa muy limpia a guardar referencias a cada listener para limpiarlas manualmente. Un solo abort() desuscribe todos los eventos que comparten esa señal.
Combinar señales: AbortSignal.any
Cuando una misma operación debe abortarse si cualquiera de varias condiciones se cumple (timeout, cancelación manual, un componente padre que se desmonta), se usa AbortSignal.any([s1, s2, ...]):
async function loadWithCancel(url, { userCancel, ms = 5000 } = {}) {
const signal = AbortSignal.any([
userCancel,
AbortSignal.timeout(ms)
]);
const res = await fetch(url, { signal });
return res.json();
}
La señal resultante aborta en cuanto aborta la primera de las originales. Es la composición natural de varias fuentes de cancelación.
Comprobar y propagar la cancelación
Dentro de una función asíncrona compleja es útil comprobar periódicamente si alguien ha abortado, o propagar el error de inmediato:
async function processItems(items, signal) {
for (const item of items) {
signal.throwIfAborted(); // lanza si signal.aborted === true
await processOne(item, signal);
}
}
throwIfAborted() lanza un error con signal.reason si la señal está abortada; en caso contrario no hace nada. Si no quieres lanzar y prefieres salir limpiamente, simplemente comprueba signal.aborted:
async function gentleLoop(signal) {
while (!signal.aborted) {
await doWork();
}
}
Para reaccionar a la cancelación desde un oyente, la señal emite un evento abort:
signal.addEventListener("abort", () => {
console.log("Cancelado:", signal.reason);
});
Es útil para liberar recursos (cerrar un stream, un WebSocket, un cursor de base de datos) en cuanto llega la petición de cancelación.
Patrón completo: búsqueda con timeout y botón cancelar
Un ejemplo que combina varias piezas a la vez:
function search(query, { userCancel } = {}) {
const timeoutSignal = AbortSignal.timeout(4000);
const signal = userCancel
? AbortSignal.any([userCancel, timeoutSignal])
: timeoutSignal;
return fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal })
.then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
.catch(err => {
if (err.name === "AbortError") {
const reason = signal.reason;
if (reason?.message?.startsWith("Timeout") || reason?.name === "TimeoutError") {
throw new Error("La búsqueda tardó demasiado");
}
throw new Error("Búsqueda cancelada");
}
throw err;
});
}
Una única señal recoge timeout y cancelación manual. El catch puede distinguir la causa a partir de signal.reason.
APIs que aceptan señales
Cada vez más APIs aceptan { signal } en sus opciones. Algunas que conviene conocer:
fetch(url, { signal })- peticiones HTTP.setTimeout(fn, ms, { signal })- en Node.js 20+; el temporizador se cancela al abortar.addEventListener(type, fn, { signal })- quita el listener al abortar.Request,ResponseyReadableStreamen plataformas web.- Librerías de base de datos (
pg,mongodb) y HTTP client (axios,undici) exponensignalen sus operaciones. La regla es simple: si una API bloqueante aceptasignal, aprovéchala. Es mucho mejor que el operación termine limpiamente a seguir ejecutándose y descartar el resultado al final.
Buenas prácticas
- Siempre distingue
AbortErrordel resto en loscatch; una cancelación no es un fallo. - Usa
AbortSignal.timeout(ms)para timeouts simples; reserva el comboAbortController + setTimeoutpara cuando necesites el controlador para algo más (por ejemplo, también cancelar desde un botón). - En componentes con ciclo de vida (React, Vue, web components), crea un
AbortControlleral montar y llama aabort()al desmontar: todos los listeners y peticiones asociados se cancelan de una vez. - Pasa la
signala funciones auxiliares y compruebasignal.abortedothrowIfAborted()en bucles largos; sin eso, el abort no tiene efecto. - Si ofreces funciones que aceptan cancelación, documenta la opción
signalen la firma y tradúcela a las operaciones internas cuando sea posible. - Combina señales con
AbortSignal.anyen vez de propagar varios controladores a mano; el código queda mucho más limpio.
Resumen
AbortController y AbortSignal han convertido la cancelación en una pieza de primera clase en JavaScript. Con un par de líneas es posible detener un fetch, imponer un timeout, cancelar desde la interfaz o limpiar decenas de listeners con una única llamada. AbortSignal.timeout y AbortSignal.any cubren los escenarios más comunes con código declarativo y componible. Integrarlo desde el primer momento en cualquier función asíncrona no trivial evita fugas de recursos, carreras entre respuestas y UIs que siguen mostrando estado obsoleto tras una cancelación.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, JavaScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de JavaScript
Explora más contenido relacionado con JavaScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Entender el rol del par
AbortController/AbortSignal. - Cancelar un
fetchpasandosignaly capturar elAbortError. - Implementar timeouts con
setTimeout+controller.abort()oAbortSignal.timeout(ms). - Cancelar manualmente operaciones desde la UI (botón cancelar).
- Eliminar listeners de forma automática con
addEventListener({ signal }). - Componer varias señales con
AbortSignal.any([...])para combinar cancelaciones.