SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON —— casi siempre significa una sola cosa: tu código llamó a JSON.parse() (o res.json()) sobre una página HTML, no JSON. El < en posición 0 es la apertura de <!DOCTYPE html> o <html>. Aquí están exactamente las causas y cómo arreglarlo.
Cómo se ve el error
// V8 (Chrome / Node / Edge)
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
// V8 más antiguo
SyntaxError: Unexpected token < in JSON at position 0
// Firefox
SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
// Safari
SyntaxError: JSON Parse error: Unexpected identifier "<"Posición 0 es la pista clave: el primer byte ya está mal, así que la respuesta nunca fue JSON para empezar.
Por qué pasa: recibiste HTML
Una llamada fetch o AJAX devolvió un documento HTML en vez del JSON esperado. Las fuentes habituales:
- Una página de error 404 / 500 servida como HTML por tu framework o servidor web
- Una redirección de login o auth a una página HTML (sesión caducada)
- Una URL equivocada —— pegándole a la ruta del app/SPA en vez del endpoint de la API
- Un proxy, CDN o captive portal devolviendo su propia página HTML
Ejemplo roto
const res = await fetch('/api/user'); // el servidor devolvió una página HTML 404
const data = await res.json(); // ❌ Unexpected token '<' …
// res.json() intenta parsear "<!DOCTYPE html>…" como JSONEjemplo arreglado
const res = await fetch('/api/user');
// 1) Comprueba el estado antes de parsear
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
// 2) Opcional: confirma que realmente es JSON
const type = res.headers.get('content-type') ?? '';
if (!type.includes('application/json')) {
const text = await res.text();
throw new Error(`Expected JSON, got: ${text.slice(0, 80)}…`);
}
const data = await res.json(); // ✅ seguroDos fuentes menos obvias que vale conocer
- Respuestas bloqueadas por WAF o rate-limit. Un WAF, CDN o capa de protección DDoS puede interceptar la llamada y devolver su propia página HTML de challenge / bloqueo —— el interstitial de Cloudflare, la página de denegación de AWS WAF, la pared de un captive-portal de Wi-Fi público. El estado suele ser
403/429/503con cuerpo HTML. Inspecciona la respuesta en la pestaña Network y mira si hay headers comoCF-RayoX-Amz-Cf-Idpara detectarlo. - El fallback de historia de la SPA devolviendo
index.html. Los hosts estáticos y servidores de desarrollo suelen reescribir cualquier ruta desconocida a/index.htmlpara que el router del cliente tome el control. Si tu front-end hace fetch de/api/usercomo ruta relativa sin querer y el proxy no está configurado, el servidor devuelve silenciosamente el shell HTML de tu SPA con un200—— y obtienes una versión confusa «200 OK pero es HTML» del mismo error.
Cómo arreglarlo —— paso a paso
- Loguea el body crudo:
console.log(await res.clone().text()). Si empieza con<!DOCTYPEo<html>, tienes HTML. - Comprueba
res.ok/res.statusantes de llamar ares.json()—— un 4xx/5xx normalmente devuelve una página HTML de error. - Verifica la URL en la pestaña Network. Una ruta relativa puede resolverse al index.html de tu SPA en vez de a la API.
- Revisa la autenticación —— una redirección a la página de login significa que tu token/sesión caducó.
- Inspecciona
Content-Type—— ramifica sobre él para manejar respuestas no-JSON en vez de parsearlas a ciegas.
Preguntas frecuentes
¿Qué significa «Unexpected token < in JSON at position 0»?
El parser encontró < como primer carácter, lo que nunca es JSON válido. La respuesta era HTML (una página <!DOCTYPE> o <html>), no el JSON que esperaba tu código.
¿Por qué mi fetch devuelve HTML en vez de JSON?
Causas comunes: página de error 404/500, redirección de auth a una página de login, o URL equivocada que resuelve a tu front-end en vez de a la API. Comprueba res.ok y loguea await res.text() para ver qué volvió.
¿Cómo evito que rompa mi app?
Guard antes de parsear: comprueba res.ok y el header Content-Type, y envuelve JSON.parse()/res.json() en try/catch para que una respuesta HTML se convierta en un error manejado en vez de una excepción.
Arréglalo ahora
Si tienes un body de respuesta del que no estás seguro, pégalo en JSON Fix —— te dice al momento si es JSON válido o algo más (como HTML). Todo corre en tu navegador.
- JSON Fix —— valida y repara JSON en tu navegador
- Cómo arreglar los errores «Unexpected Token» de JSON.parse —— cada variante de token
- Unexpected token u in JSON at position 0 —— el error hermano de
undefined - Arreglar «[object Object] is not valid JSON» —— la referencia completa de errores de sintaxis