Cancelando Solicitudes Fetch en Vuelo con AbortController

Las aplicaciones web modernas realizan frecuentemente solicitudes HTTP que los usuarios podrían no necesitar completar. Un usuario escribiendo en un campo de búsqueda genera múltiples solicitudes, pero solo la más reciente importa. Un usuario navegando fuera de una página hace que cualquier solicitud pendiente para esa página sea irrelevante. Sin una cancelación adecuada, estas solicitudes innecesarias desperdician ancho de banda, consumen recursos del servidor y pueden causar que datos obsoletos aparezcan en tu interfaz de usuario.
La API AbortController proporciona una forma limpia y estandarizada de cancelar solicitudes fetch y otras operaciones asíncronas. Este artículo te muestra cómo implementar la cancelación de solicitudes usando AbortController, cubriendo patrones prácticos como el debouncing de búsquedas, limpieza de componentes y manejo de timeouts.
Puntos Clave
- AbortController crea una señal que fetch monitorea para eventos de cancelación
- Siempre verifica AbortError en bloques catch para distinguir cancelaciones de fallos
- Cancela solicitudes previas en interfaces de búsqueda para prevenir condiciones de carrera
- Usa funciones de limpieza en componentes de frameworks para cancelar solicitudes al desmontarse
- Combina AbortController con setTimeout para funcionalidad de timeout de solicitudes
- Cada AbortController es de un solo uso - crea nuevos para lógica de reintento
- Node.js 18+ incluye soporte nativo para AbortController
Qué Hacen AbortController y AbortSignal
AbortController crea un objeto controlador que gestiona la cancelación a través de un AbortSignal asociado. El controlador tiene un trabajo: llamar a abort()
cuando quieres cancelar una operación. La señal actúa como un canal de comunicación que fetch y otras APIs monitorean para eventos de cancelación.
const controller = new AbortController()
const signal = controller.signal
// La señal comienza como no abortada
console.log(signal.aborted) // false
// Llamar abort() cambia el estado de la señal
controller.abort()
console.log(signal.aborted) // true
Cuando pasas una señal a fetch y posteriormente llamas a abort()
, la promesa de fetch se rechaza con una DOMException
llamada AbortError
. Esto te permite distinguir entre cancelaciones y fallos reales de red.
Implementación Básica de AbortController
Para cancelar una solicitud fetch, crea un AbortController, pasa su señal a fetch, luego llama a abort()
cuando sea necesario:
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log('Datos recibidos:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('La solicitud fue cancelada')
} else {
console.error('La solicitud falló:', error)
}
})
// Cancelar la solicitud
controller.abort()
Los puntos clave:
- Crea el controlador antes de hacer la solicitud
- Pasa
controller.signal
a las opciones de fetch - Llama a
controller.abort()
para cancelar - Verifica AbortError en tu bloque catch
Cómo Cancelar Solicitudes de Búsqueda Cuando los Usuarios Escriben
Las interfaces de búsqueda a menudo disparan solicitudes en cada pulsación de tecla. Sin cancelación, las respuestas lentas pueden llegar fuera de orden, mostrando resultados para consultas obsoletas. Aquí se muestra cómo cancelar solicitudes de búsqueda previas:
let searchController = null
function performSearch(query) {
// Cancelar cualquier búsqueda existente
if (searchController) {
searchController.abort()
}
// Crear nuevo controlador para esta búsqueda
searchController = new AbortController()
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: searchController.signal
})
.then(response => response.json())
.then(results => {
console.log('Resultados de búsqueda:', results)
updateSearchUI(results)
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Búsqueda cancelada')
} else {
console.error('Búsqueda falló:', error)
showSearchError()
}
})
}
// Uso: cada nueva búsqueda cancela la anterior
performSearch('javascript')
performSearch('javascript frameworks') // Cancela la búsqueda de 'javascript'
Este patrón asegura que solo los resultados de búsqueda más recientes aparezcan en tu interfaz de usuario, previniendo condiciones de carrera donde solicitudes más lentas sobrescriben las más rápidas.
Cancelando Solicitudes al Desmontar Componentes
En frameworks frontend, los componentes a menudo hacen solicitudes fetch que deberían cancelarse cuando el componente se desmonta. Esto previene memory leaks y errores por intentar actualizar componentes desmontados.
Ejemplo en React
import { useEffect, useState } from 'react'
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const controller = new AbortController()
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const userData = await response.json()
setUser(userData)
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Falló al obtener usuario:', error)
}
} finally {
if (!controller.signal.aborted) {
setLoading(false)
}
}
}
fetchUser()
// La función de limpieza cancela la solicitud
return () => controller.abort()
}, [userId])
if (loading) return <div>Cargando...</div>
return <div>{user?.name}</div>
}
Ejemplo en Vue
export default {
data() {
return {
user: null,
controller: null
}
},
async mounted() {
this.controller = new AbortController()
try {
const response = await fetch(`/api/users/${this.userId}`, {
signal: this.controller.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
this.user = await response.json()
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Falló al obtener usuario:', error)
}
}
},
beforeUnmount() {
if (this.controller) {
this.controller.abort()
}
}
}
Implementando Timeouts de Solicitud con AbortController
Las solicitudes de red pueden colgarse indefinidamente. AbortController combinado con setTimeout
proporciona un mecanismo de timeout limpio:
function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController()
const timeoutId = setTimeout(() => {
controller.abort()
}, timeoutMs)
return fetch(url, {
...options,
signal: controller.signal
}).finally(() => {
clearTimeout(timeoutId)
})
}
// Uso
fetchWithTimeout('/api/slow-endpoint', {}, 3000)
.then(response => response.json())
.then(data => console.log('Datos:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('La solicitud expiró')
} else {
console.error('La solicitud falló:', error)
}
})
Para lógica de timeout reutilizable, crea una función utilitaria:
function createTimeoutSignal(timeoutMs) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
// Limpiar timeout cuando se usa la señal
controller.signal.addEventListener('abort', () => {
clearTimeout(timeoutId)
}, { once: true })
return controller.signal
}
// Uso
fetch('/api/data', { signal: createTimeoutSignal(5000) })
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
console.log('La solicitud expiró o fue cancelada')
}
})
Manejo Adecuado de Errores para Solicitudes Canceladas
Siempre distingue entre cancelaciones y errores genuinos. AbortError indica cancelación intencional, no un fallo:
async function handleRequest(url) {
const controller = new AbortController()
try {
const response = await fetch(url, { signal: controller.signal })
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
} catch (error) {
if (error.name === 'AbortError') {
// Esto es esperado cuando cancelamos - no registrar como error
console.log('La solicitud fue cancelada')
return null
}
// Este es un error real que necesita manejo
console.error('La solicitud falló:', error)
throw error
}
}
Para aplicaciones con seguimiento de errores, excluye AbortError de los reportes de error:
.catch(error => {
if (error.name === 'AbortError') {
// No reportar cancelaciones al seguimiento de errores
return
}
// Reportar errores reales
errorTracker.captureException(error)
throw error
})
Gestionando Múltiples Solicitudes
Cuando manejas múltiples solicitudes concurrentes, podrías necesitar cancelarlas todas a la vez o gestionarlas individualmente:
class RequestManager {
constructor() {
this.controllers = new Map()
}
async fetch(key, url, options = {}) {
// Cancelar solicitud existente con la misma clave
this.cancel(key)
const controller = new AbortController()
this.controllers.set(key, controller)
try {
const response = await fetch(url, {
...options,
signal: controller.signal
})
return response
} finally {
this.controllers.delete(key)
}
}
cancel(key) {
const controller = this.controllers.get(key)
if (controller) {
controller.abort()
this.controllers.delete(key)
}
}
cancelAll() {
for (const controller of this.controllers.values()) {
controller.abort()
}
this.controllers.clear()
}
}
// Uso
const requestManager = new RequestManager()
// Estas solicitudes pueden gestionarse independientemente
requestManager.fetch('user-profile', '/api/user/123')
requestManager.fetch('user-posts', '/api/user/123/posts')
// Cancelar solicitud específica
requestManager.cancel('user-profile')
// Cancelar todas las solicitudes pendientes
requestManager.cancelAll()
Compatibilidad con Navegadores y Node.js
AbortController tiene excelente soporte en navegadores:
- Chrome 66+
- Firefox 57+
- Safari 12.1+
- Edge 16+
Para Node.js, AbortController está disponible nativamente en Node 18+. Para versiones anteriores, usa el polyfill abort-controller:
npm install abort-controller
// Para Node.js < 18
const { AbortController } = require('abort-controller')
// Usar normalmente
const controller = new AbortController()
Conclusión
AbortController proporciona una forma limpia y estandarizada de cancelar solicitudes fetch en aplicaciones web modernas. Los patrones clave son: crear un controlador antes de cada solicitud, pasar su señal a fetch, y llamar a abort() cuando se necesite cancelación. Siempre maneja AbortError por separado de fallos genuinos de red para evitar tratar cancelaciones intencionales como errores.
Los casos de uso más comunes—gestión de solicitudes de búsqueda, limpieza de componentes y manejo de timeouts—siguen patrones directos que puedes adaptar a tus necesidades específicas. Con una implementación adecuada, la cancelación de solicitudes mejora tanto la experiencia del usuario como el rendimiento de la aplicación al prevenir actividad de red innecesaria y actualizaciones de datos obsoletos.
¿Listo para implementar cancelación de solicitudes en tu aplicación? Comienza con el patrón básico para tu caso de uso más común, luego expande a escenarios más complejos según sea necesario. La inversión en gestión adecuada de solicitudes paga dividendos en rendimiento de aplicación y experiencia del usuario.
Preguntas Frecuentes
No, una vez que se llama abort(), la señal del controlador permanece en estado abortado permanentemente. Debes crear un nuevo AbortController para cada nueva solicitud o intento de reintento.
No, abort() solo previene que tu código JavaScript procese la respuesta. Si la solicitud ya ha sido enviada por la red, el servidor aún puede recibirla y procesarla.
Crea múltiples controladores y guárdalos en un array o Map, luego llama abort() en cada uno. Alternativamente, usa un solo controlador y pasa su señal a múltiples llamadas fetch - llamar abort() una vez las cancelará todas.
No pasa nada. La llamada abort() se ignora si la solicitud ya ha terminado exitosamente o ha fallado por otras razones.
Sí, AbortController funciona idénticamente con tanto cadenas de Promise como async/await. La promesa fetch se rechazará con un AbortError cuando se cancele, lo cual puedes capturar con bloques try/catch.