El error no dice que tu servidor respondió mal. Dice que tu petición nunca llegó. Esa diferencia es todo lo que necesitas para arreglarlo.
Son las once de la noche y llevas cuarenta minutos revisando el código de tu endpoint.
La consola solo dice esto:
TypeError: Failed to fetch
Y aquí está el detalle que casi todo el mundo pasa por alto. Esa distinción te ahorra la mitad del tiempo de depuración, porque cambia por completo dónde tienes que buscar.
Un 404 no lanza este error. Un 500 tampoco. Cuando el servidor responde, aunque responda mal, fetch() cumple su promesa sin problema y te deja un objeto Response con ok: false. El error que estás viendo es distinto: el navegador ni siquiera pudo entregar la petición, o se negó a dejarte leer la respuesta.
Así que deja de revisar tu controlador. El problema está antes.
Qué significa exactamente el error failed to fetch
fetch() devuelve una promesa. Esa promesa se rechaza con un TypeError únicamente en dos situaciones: cuando la petición no pudo salir o llegar, y cuando el navegador bloqueó tu acceso a la respuesta por política de seguridad.
Todo lo demás, cualquier código de estado entre 200 y 599, se resuelve correctamente.
Por eso este código te engaña:
const res = await fetch(url);
const data = await res.json(); // esto sí revienta con un 404
Si esperabas que fetch lanzara una excepción con un 404, no lo hace. Comprueba siempre res.ok antes de parsear.
Failed to fetch no es un error de tu backend. Es un error de que tu backend nunca se enteró.
Las cinco causas más comunes y cómo solucionar cada una
Ordenadas por frecuencia real, no por orden alfabético.
1. CORS, el sospechoso número uno
Si tu petición sale de un origen y va hacia otro, el navegador exige permiso explícito del servidor. Sin la cabecera Access-Control-Allow-Origin correcta, el navegador descarta la respuesta y tú recibes Failed to fetch.
Lo confuso es que la petición sí llegó al servidor. Puedes verla en tus logs. El bloqueo ocurre después, en el navegador.
Cómo confirmarlo: abre la consola, no la pestaña de red. El mensaje de CORS aparece ahí, en rojo, con el origen exacto que fue rechazado. El TypeError es solo el eco.
La solución está en el servidor, siempre. Añade el origen a Access-Control-Allow-Origin, y si tu petición usa métodos distintos de GET o cabeceras personalizadas, asegúrate de responder también al preflight OPTIONS. Poner mode: "no-cors" en el cliente no arregla nada, solo te devuelve una respuesta opaca que no puedes leer.
2. La URL, el puerto o el DNS
Menos glamuroso, igual de frecuente.
Un localhost:3000 cuando el servidor escucha en el 3001. Una variable de entorno vacía que deja la URL como undefined/api/users. Un dominio mal escrito. Un servicio que se cayó hace veinte minutos.
Prueba el mismo endpoint desde la terminal con curl. Si curl también falla, el problema no está en tu JavaScript y puedes cerrar el editor.
3. Contenido mixto
Tu página se sirve por HTTPS y tu petición apunta a http://. Los navegadores modernos bloquean eso sin preguntar.
Suele aparecer al pasar de desarrollo local a un entorno desplegado, y sorprende porque en local todo funcionaba.
La solución es servir el endpoint por HTTPS. Un certificado autofirmado en tu API tiene el mismo efecto: el navegador rechaza la conexión y tú ves Failed to fetch sin más pistas.
4. Algo entre tu navegador y la red
Un bloqueador de anuncios que filtra por patrón de URL. Una extensión de privacidad. Una VPN corporativa. Un firewall que no deja salir ese puerto.
Esta causa tiene una señal muy clara: funciona en ventana de incógnito con las extensiones desactivadas, o funciona en otro navegador, o funciona desde otra red.
Si tu URL contiene palabras como ads, track, analytics o pixel, un bloqueador la va a matar aunque tu endpoint sea completamente legítimo. Es más común de lo que parece.
5. La petición se canceló antes de terminar
Un AbortController que se dispara. Un componente de React que se desmonta y cancela la petición en el cleanup del useEffect. El usuario que recarga la página a mitad de la llamada. Un timeout del lado del cliente.
Todo eso produce el mismo TypeError, y es la causa más difícil de ver porque el código es correcto. Solo se ejecutó en el orden equivocado.
Distingue el caso comprobando error.name === "AbortError" en tu catch antes de tratarlo como fallo real.
Cuándo es un problema de red y cuándo es de código
Tres comprobaciones, en este orden, y en menos de dos minutos sabes de qué lado está el problema.
curldesde la terminal. Si falla, es red, servidor o URL, y no es tu JavaScript.- Si
curlfunciona pero el navegador no, es casi con total seguridad CORS o contenido mixto. El navegador aplica reglas quecurlignora. - Incógnito sin extensiones. Si ahí funciona, tienes un bloqueador o una extensión interfiriendo.
Si curl funciona y el navegador no, el problema es una política del navegador, no una caída del servidor.
El mismo error en Node.js, que no es el mismo error
Aquí es donde mucha gente se pierde, sobre todo si trabaja con agentes de IA o servicios en el servidor.
En Node.js el mensaje es ligeramente distinto: TypeError: fetch failed. Y en Node no hay CORS, así que puedes descartar la causa número uno de inmediato.
Lo importante es que Node adjunta el motivo real en error.cause. Imprímelo:
catch (e) { console.error(e.cause); }
Ahí verás lo que de verdad pasó: ECONNREFUSED si nada escucha en ese puerto, ENOTFOUND si el DNS no resuelve, o un error de certificado si el TLS falló. Esa propiedad convierte un error genérico en un diagnóstico exacto.
Este es exactamente el caso que aparece cuando un agente local intenta hablar con Ollama y el servicio no está levantado. Lo explicamos a fondo en nuestra guía sobre el error fetch failed con Ollama, que cubre la configuración de host y puerto que casi siempre es la culpable.
Un último apunte antes de que vuelvas a tu consola
Failed to fetch tiene mala fama por ser vago, pero en realidad es honesto. Te está diciendo la única cosa que sabe con certeza: la petición no completó el viaje.
Lo que no te dice es por qué, y eso no es un defecto de la API. Es que el navegador, por diseño de seguridad, no te cuenta los detalles de una petición que bloqueó. Si lo hiciera, cualquier página podría sondear tu red interna.
Así que el mensaje seguirá siendo genérico. La habilidad no está en descifrarlo, está en saber qué comprobar primero.
Si quieres dejar de mantener este tipo de errores por tu cuenta, empieza gratis en BetterClaw. Un agente con todas las funciones, sin tarjeta. El plan Pro cuesta 49 dólares al mes por cinco agentes, conectores ilimitados y 90 días de memoria, con un 20 por ciento de descuento en el plan anual. Precios completos aquí. También puedes pegar cualquier error en nuestro decodificador de errores de agentes y obtener la causa concreta.
Preguntas frecuentes
¿Qué significa el error failed to fetch?
Significa que la petición hecha con fetch() no llegó a completarse a nivel de red, o que el navegador bloqueó el acceso a la respuesta por una política de seguridad. No significa que el servidor devolviera un error, porque un 404 o un 500 se resuelven correctamente y hay que comprobarlos con res.ok.
¿En qué se diferencia de "fetch failed" en Node.js?
Es el mismo tipo de fallo pero en un entorno distinto y con un mensaje ligeramente distinto. En Node no existe CORS, así que esa causa queda descartada, y además el error incluye la propiedad error.cause con el motivo exacto, normalmente ECONNREFUSED, ENOTFOUND o un fallo de certificado.
¿Cómo soluciono un failed to fetch causado por CORS?
La solución siempre está en el servidor, no en el cliente. Añade el origen que hace la petición a la cabecera Access-Control-Allow-Origin y responde correctamente a la petición preflight OPTIONS si usas métodos distintos de GET o cabeceras personalizadas. Usar mode: "no-cors" no lo arregla, solo devuelve una respuesta opaca que no puedes leer.
¿Cuánto tiempo lleva identificar la causa?
Normalmente menos de dos minutos si sigues el orden correcto: prueba con curl, luego compara navegador contra terminal, y luego abre una ventana de incógnito sin extensiones. Esas tres comprobaciones separan un problema de red de uno de configuración del navegador antes de tocar una sola línea de código.
¿Es seguro ignorar este error con un try catch vacío?
No, y es un patrón que esconde caídas reales de servicio durante semanas. Como mínimo distingue el caso de cancelación comprobando error.name === "AbortError", registra el resto con contexto suficiente, e imprime error.cause si estás en Node. Un error silenciado no desaparece, solo se vuelve invisible hasta que un usuario lo reporta.




