TypeError: Failed to fetch es el navegador diciendo que una llamada a fetch() nunca se completó. No que fallara - que nunca llegó lo bastante lejos como para tener resultado alguno.

Esa distinción importa más que cualquier otra cosa de esta página, porque descarta casi todo lo que uno sospecha primero. Un 404 no produce esto. Un 500 tampoco. Esas son peticiones completadas: el servidor respondió, fetch() se resuelve y response.ok es false. Failed to fetch significa que no hay ningún objeto de respuesta que inspeccionar, que es exactamente por lo que el mensaje te dice tan poco.

Chrome, Firefox y Safari lo redactan cada uno a su manera - Failed to fetch, NetworkError when attempting to fetch resource, Load failed - y significan lo mismo.

Las causas, en el orden en que ocurren de verdad

CORS. La más frecuente con diferencia. La petición salió, el servidor respondió y el navegador se negó a entregar la respuesta a tu código porque las cabeceras no lo permitían. La consola imprime un mensaje aparte sobre la política de origen, y el fetch se rechaza con el error genérico. Si solo lees el rechazo no aprendes nada; la línea de encima es la respuesta de verdad.

La conexión nunca llegó a ocurrir. Sin red, fallo de DNS, host inalcanzable, servidor que no está escuchando. Este es el mismo terreno que la familia ERR_CONNECTION, y el panel de red lo nombra.

Algo la bloqueó. Un bloqueador de anuncios, una extensión de privacidad, un proxy corporativo. Las peticiones a cualquier cosa que parezca analítica o rastreo se rechazan antes de salir del navegador, y el panel de red las marca como bloqueadas. Esto es invisible en tu propia máquina si eres el único sin esa extensión.

Contenido mixto. Una página https pidiendo una URL http. El navegador lo bloquea sin más.

La petición se canceló. La página navegó a otro sitio, un componente se desmontó, saltó un AbortController, se cerró la pestaña a mitad de la petición. Algo corriente en una aplicación de página única, y en el seguimiento de errores aparece como una avalancha de fallos que no le pasaron a nadie.

Una URL mala. Una errata, una ruta relativa que resuelve donde no debía, una variable de entorno que estaba vacía al construir, así que la petición fue a undefined/api/thing.

Failed to fetch dynamically imported module

Una variante concreta que merece su propio párrafo, porque la causa no está en tu código y el arreglo tampoco.

Una aplicación construida con división de código carga fragmentos con nombres de archivo con hash. Despliegas, los hashes cambian, los archivos viejos dejan de existir. Cualquiera que ya tuviera la página abierta - una pestaña olvidada toda la noche, un teléfono que nunca recargó - pide un nombre de archivo que ahora es un 404, y recibe este error en cuanto navega a una ruta cuyo fragmento aún no se ha cargado.

Dos arreglos, y funcionan juntos. Conserva un tiempo los recursos de la versión anterior en lugar de reemplazarlos, para que las pestañas abiertas sigan funcionando. Y captura el fallo en el enrutador: si una importación dinámica se rechaza, ofrece recargar, porque recargar lo arregla de verdad.

Cómo averiguar cuál de ellas tienes

La consola no te lo va a decir. El panel de red sí, y lleva unos diez segundos.

Ábrelo, reproduce el fallo y busca la petición en rojo. Después lee tres columnas:

  • Estado. Vacío o (failed) significa que la petición nunca se completó. (blocked:...) nombra lo que la bloqueó.
  • El host. Tu propio dominio, o el de otro. Un fallo contra un tercero es un problema distinto de un fallo contra tu propia API.
  • El mensaje de consola justo encima del rechazo. Si menciona un origen o un preflight, tienes un problema de CORS y todo lo demás de esta página es una distracción.

Si la petición no aparece siquiera en el panel de red, nunca salió del navegador: contenido mixto, una extensión o una URL que nunca fue válida.

Session Replay

Extensión gratuita de Chrome. Un clic en la página que se está portando mal captura la captura de pantalla, la consola y el registro de red, y te devuelve un enlace para pegar en el ticket.

Instalar la extensión

Por qué es el peor error para recibir de segunda mano

Todo lo anterior da por hecho que puedes reproducirlo. La mayoría de las veces no puedes, porque las causas que producen este error son las que viven en la máquina de otra persona: su extensión, su red, su proxy corporativo, su pestaña rancia.

Así que llega como “la página solo dice que algo ha ido mal”, y el mensaje de consola que quizá te citen, Failed to fetch, es el único mensaje de la página que no lleva información. La prueba que zanjaría el asunto es una fila de un panel de red que nadie está mirando.

Ese es el argumento para capturar el registro de red en el momento en que ocurre en lugar de hacer preguntas después. Un archivo HAR es la manera manual de hacerlo. En cualquier caso, lo que zanja esta clase de fallo es la fila, no la frase.

La versión corta

  • Significa que la petición nunca se completó. Sin respuesta, sin código de estado, sin nada que inspeccionar.
  • Un 404 o un 500 no lo causan - esos son viajes de ida y vuelta con éxito y respuestas desagradables.
  • Mira primero la línea de consola que hay encima del rechazo. Si menciona CORS, esa es tu respuesta.
  • Después comprueba si la petición aparece siquiera en el panel de red. Si no aparece, algo del navegador la detuvo antes de que saliera.
  • Después de un despliegue, “failed to fetch dynamically imported module” significa que una pestaña vieja está pidiendo un archivo que tu última versión borró.