Fetch avanzado y AbortController

Avanzado
JavaScript
JavaScript
Actualizado: 18/04/2026

Cancelar peticiones con AbortController

En aplicaciones reales, las peticiones HTTP rara vez son operaciones aisladas que deben completarse a toda costa. Un usuario que escribe en un buscador dispara una petición por letra y solo la última debe entregar resultados; un componente que se desmonta no necesita ya la respuesta pendiente; un timeout debe interrumpir una llamada que tarda demasiado. Para todos estos escenarios, JavaScript proporciona un mecanismo estándar: AbortController y las señales AbortSignal.

La mecánica es simple. Creas un controlador, obtienes su signal y se la pasas a fetch en las opciones. Cuando invocas controller.abort(), todas las operaciones asociadas a esa señal se cancelan inmediatamente.

const controller = new AbortController();

fetch("https://api.ejemplo.com/datos", { signal: controller.signal })
    .then(res => res.json())
    .then(datos => console.log(datos))
    .catch(err => {
        if (err.name === "AbortError") {
            console.log("Peticion cancelada");
        } else {
            console.error("Error real:", err);
        }
    });

// Cancelar despues de 500 ms
setTimeout(() => controller.abort(), 500);

Al cancelar, fetch rechaza la promesa con un AbortError. Es importante distinguir este error del resto: un AbortError es una cancelación intencional, no un fallo de red. Manejar ambos casos de forma idéntica lleva a falsos positivos en logs y alertas de producción.

Toda función async pública que haga peticiones debería aceptar una opción signal para permitir al consumidor cancelarla sin acoplamientos.

Patrón básico en servicios

Una práctica recomendable es propagar la señal a través de las capas. La función que hace el fetch no crea su propio AbortController: recibe una AbortSignal externa y la encadena.

async function obtenerUsuario(id, { signal } = {}) {
    const respuesta = await fetch(`/api/users/${id}`, { signal });
    if (!respuesta.ok) {
        throw new Error(`HTTP ${respuesta.status}`);
    }
    return respuesta.json();
}

// El controlador lo crea quien decide cancelar
const controller = new AbortController();
obtenerUsuario(42, { signal: controller.signal });
// ... eventualmente
controller.abort();

Este patrón funciona igual de bien en hooks de React, en un AbortController asociado a un componente Angular, o en un router de Vue. La señal se cancela cuando el componente se desmonta, liberando recursos automáticamente.

Timeouts declarativos con AbortSignal.timeout

Antes de los timeouts nativos, el patrón habitual era combinar setTimeout con controller.abort(). Hoy existe AbortSignal.timeout(ms), que devuelve directamente una señal que se dispara tras el tiempo indicado.

async function cargarConTimeout(url, ms = 5000) {
    try {
        const respuesta = await fetch(url, { signal: AbortSignal.timeout(ms) });
        return await respuesta.json();
    } catch (err) {
        if (err.name === "TimeoutError") {
            throw new Error(`Peticion excedio ${ms} ms`);
        }
        throw err;
    }
}

La señal producida por AbortSignal.timeout lanza un TimeoutError cuando expira. Esto permite distinguir cancelaciones manuales (AbortError) de expiraciones (TimeoutError) en el mismo bloque catch.

AbortSignal.timeout es más declarativo y seguro que montar el timeout a mano: no hay que limpiar el setTimeout, no hay fugas si la petición termina antes.

Combinar señales con AbortSignal.any

Algunas operaciones deben cancelarse por varias razones simultáneas: un timeout global, el desmontaje del componente, una cancelación manual del usuario. AbortSignal.any([signals]) combina varias señales en una sola que se activa en cuanto cualquiera de ellas se cancela.

async function obtenerConMultiplesSenales(url, { signal: externa } = {}) {
    const timeout = AbortSignal.timeout(10000);
    const combinada = AbortSignal.any([timeout, externa].filter(Boolean));

    const respuesta = await fetch(url, { signal: combinada });
    return respuesta.json();
}

La función anterior respeta tanto el timeout interno como la señal externa. Si cualquiera de las dos se activa, la petición se cancela. El filtrado con .filter(Boolean) evita errores cuando el consumidor no pasa señal externa.

Reintentos con backoff exponencial

Una red perfecta no existe. Un 503 Service Unavailable transitorio, un timeout por saturación momentánea, una conexión inestable: todos son candidatos ideales para reintentar. La estrategia estándar es backoff exponencial: cada reintento espera más que el anterior, reduciendo la presión sobre el servidor.

async function fetchConReintentos(url, opciones = {}) {
    const {
        intentosMaximos = 3,
        baseMs = 300,
        signal,
        ...resto
    } = opciones;

    let ultimoError;
    for (let intento = 1; intento <= intentosMaximos; intento += 1) {
        try {
            const respuesta = await fetch(url, { ...resto, signal });
            if (respuesta.ok) return respuesta;

            if (respuesta.status >= 500) {
                ultimoError = new Error(`HTTP ${respuesta.status}`);
            } else {
                return respuesta; // 4xx no se reintenta
            }
        } catch (err) {
            if (err.name === "AbortError") throw err; // Respetar cancelacion
            ultimoError = err;
        }

        const espera = baseMs * Math.pow(2, intento - 1);
        await esperar(espera, signal);
    }
    throw ultimoError;
}

function esperar(ms, signal) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(resolve, ms);
        signal?.addEventListener("abort", () => {
            clearTimeout(timer);
            reject(new DOMException("Abort", "AbortError"));
        });
    });
}

Puntos clave del patrón:

  • 1. Solo se reintentan errores de red y códigos 5xx. Los 4xx (400, 401, 403, 404) son fallos del cliente que no mejorarán con un reintento.

  • 2. La cancelación se respeta siempre. Si la señal externa se activa durante un reintento o durante la espera, la función aborta inmediatamente.

  • 3. La espera es cancelable. El helper esperar escucha la señal para que un abort no deje al código atrapado esperando el timer.

Reintentar sin backoff sobre un servidor caído solo acelera su caída. El backoff exponencial es una cortesía profesional.

Jitter para evitar manadas

Cuando muchos clientes reintentan al mismo tiempo sincronizados (tras un pico de error compartido), crean una manada que sobrecarga aún más el servidor. La solución es anadir jitter (aleatoriedad) a la espera.

const esperaBase = baseMs * Math.pow(2, intento - 1);
const jitter = Math.random() * 0.3 * esperaBase; // 30% de variacion
const espera = esperaBase + jitter;

Con jitter, cada cliente espera un poco distinto, y los reintentos se distribuyen en el tiempo.

Flujo completo con cancelación en un buscador

Un caso típico que combina todo lo anterior es un buscador con autocomplete. Cada letra dispara una petición; cuando llega una nueva letra antes de que la anterior responda, la previa se cancela.

let controladorActual = null;

async function buscar(termino) {
    controladorActual?.abort();
    controladorActual = new AbortController();

    try {
        const timeout = AbortSignal.timeout(3000);
        const senal = AbortSignal.any([controladorActual.signal, timeout]);

        const url = `/api/search?q=${encodeURIComponent(termino)}`;
        const respuesta = await fetch(url, { signal: senal });
        const datos = await respuesta.json();
        renderizarResultados(datos);
    } catch (err) {
        if (err.name === "AbortError") return; // Cancelacion silenciosa
        if (err.name === "TimeoutError") {
            mostrarAviso("La busqueda tardo demasiado");
            return;
        }
        mostrarError("Error al buscar");
    }
}

input.addEventListener("input", e => buscar(e.target.value));

El diseño evita race conditions: si escribes rápido "mad", "madr", "madri", "madrid" en menos de 300 ms, las primeras peticiones se cancelan antes de terminar. El usuario solo ve los resultados de la última búsqueda.

sequenceDiagram
    participant U as Usuario
    participant B as Buscador
    participant API as Servidor
    U->>B: escribe "ma"
    B->>API: GET /search?q=ma
    U->>B: escribe "mad"
    B-->>API: abort anterior
    B->>API: GET /search?q=mad
    API-->>B: resultados mad
    B->>U: muestra resultados

Integración con Promise.race

Antes de AbortSignal.timeout, el patrón para timeouts era usar Promise.race entre la petición y un temporizador. Sigue siendo útil cuando necesitas fallback a un valor por defecto, no solo cancelar.

async function conFallback(url, ms, fallback) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), ms);

    try {
        const respuesta = await fetch(url, { signal: controller.signal });
        clearTimeout(timer);
        return respuesta.json();
    } catch (err) {
        if (err.name === "AbortError") return fallback;
        throw err;
    }
}

Dominar AbortController cambia la forma de escribir código asíncrono: pasa de ser una colección de promesas que esperas que no fallen a un conjunto de operaciones cancelables compuestas con ciclo de vida claro. Este control es lo que diferencia un cliente HTTP amateur de uno profesional.

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

Cancelar una petición Fetch con AbortController. Aplicar timeouts con AbortSignal.timeout. Combinar múltiples señales con AbortSignal.any. Implementar reintentos con backoff exponencial. Manejar el error AbortError de forma diferenciada.