AbortController: cancelar fetch y promesas

Avanzado
JavaScript
JavaScript
Actualizado: 04/05/2026

Diagrama: tutorial-javascript-abort-controller

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 fetch al abortarse no es un Error cualquiera: es un DOMException con name === "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, Response y ReadableStream en plataformas web.
  • Librerías de base de datos (pg, mongodb) y HTTP client (axios, undici) exponen signal en sus operaciones. La regla es simple: si una API bloqueante acepta signal, 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 AbortError del resto en los catch; una cancelación no es un fallo.
  • Usa AbortSignal.timeout(ms) para timeouts simples; reserva el combo AbortController + setTimeout para 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 AbortController al montar y llama a abort() al desmontar: todos los listeners y peticiones asociados se cancelan de una vez.
  • Pasa la signal a funciones auxiliares y comprueba signal.aborted o throwIfAborted() en bucles largos; sin eso, el abort no tiene efecto.
  • Si ofreces funciones que aceptan cancelación, documenta la opción signal en la firma y tradúcela a las operaciones internas cuando sea posible.
  • Combina señales con AbortSignal.any en 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 - Autor del tutorial

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 fetch pasando signal y capturar el AbortError.
  • Implementar timeouts con setTimeout + controller.abort() o AbortSignal.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.