jsonerrorsdebuggingfetchapi

Unexpected Token in JSON at Position 0 — Causes & Fix

·8 min read·Error Fixes

What Does "Unexpected token < in JSON at position 0" Mean?

This JavaScript error means JSON.parse() tried to parse a string that begins with < — an HTML tag character — instead of {, [, ", or a number. Your code expected a JSON response but the server returned an HTML page.

SyntaxError: Unexpected token '<', "<!DOCTYPE"... is not valid JSON

The < at position 0 is almost always the first character of an HTML <!DOCTYPE html> declaration or an <html> tag from an error page.

Root Cause: The Server Returned HTML Instead of JSON

This error never means your JSON is malformed. It means the server returned the wrong content entirely. The five most common reasons:

Cause 1: HTTP error page (404, 500, 401)

Your API returned a 404 Not Found, 500 Internal Server Error, or 401 Unauthorized HTML error page. Most web servers and hosting platforms serve HTML error pages by default, even when the client expects JSON.

Cause 2: Wrong API endpoint URL

The URL points to a web page route instead of an API route. The server serves the page's HTML and your code tries to parse it as JSON. Common mistake: calling /users when the API is at /api/users.

Cause 3: Redirect to a login page

An authenticated API request was made after the session expired. The server redirects to /login (an HTML page), fetch follows the redirect, and the HTML login page body gets passed to .json().

Cause 4: CDN / proxy error page

A Cloudflare 503, Nginx 502 Bad Gateway, or load balancer error page is served instead of your API. These are almost always HTML.

Cause 5: Development proxy misconfiguration (Next.js, Vite, CRA)

In local dev, the API proxy is incorrectly configured. Requests that should route to your backend hit the dev server's fallback, which returns the index.html SPA shell.

How to Diagnose in 60 Seconds

Open your browser's Developer Tools (F12) → Network tab:

  1. Find the failing API request
  2. Check the Status column — is it 404, 500, 302, or 401?
  3. Click the request → Response tab — do you see HTML?
  4. Check the Request URL — is it going to the correct endpoint?
  5. Check Response HeadersContent-Type — is it text/html instead of application/json?

If the status is 200 but the Content-Type is text/html, your server is returning an HTML page thinking it is serving a success response. This often happens with catch-all route handlers in SPAs.

Fix 1: Check response.ok Before Calling .json()

The fetch API does not throw on 4xx or 5xx responses — it only rejects on network failure. Always check response.ok first:

javascript
const response = await fetch('/api/users');

if (!response.ok) {
  // Read as text so we can see the actual error page
  const text = await response.text();
  console.error(`Server returned ${response.status}:`, text.slice(0, 300));
  throw new Error(`HTTP error ${response.status}`);
}

const data = await response.json();

This pattern lets you see the exact HTML error page content in the console — which usually tells you immediately what went wrong.

Fix 2: Always Set the Accept Header

Tell the server you expect JSON, not HTML. Many frameworks check this header and return JSON errors instead of HTML error pages when they see Accept: application/json:

javascript
const response = await fetch('/api/users', {
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  },
});

Express with the express-async-errors middleware, FastAPI, and Django REST Framework all respect the Accept header.

Fix 3: Build a Robust fetchJSON Wrapper

A reusable wrapper that handles all the edge cases:

javascript
async function fetchJSON(url, options = {}) {
  const defaultHeaders = {
    "Accept": "application/json",
    "Content-Type": "application/json",
    ...options.headers,
  };

  const response = await fetch(url, { ...options, headers: defaultHeaders });
  const text = await response.text();

  // Log non-JSON response for debugging
  if (!text.trim().startsWith("{") && !text.trim().startsWith("[")) {
    console.error(`Non-JSON response from ${url} (${response.status}):`,
                  text.slice(0, 300));
    throw new Error(`Expected JSON from ${url}, got ${response.status}`);
  }

  const data = JSON.parse(text);

  if (!response.ok) {
    throw Object.assign(new Error(data.message || "API error"), {
      status: response.status,
      data,
    });
  }

  return data;
}

Fix 4: Handle Authentication Redirects

If the error happens after user inactivity, catch 401 responses before parsing:

javascript
async function apiFetch(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    credentials: "include", // send cookies for session-based auth
  });

  if (response.status === 401) {
    window.location.href = "/login?redirect=" + encodeURIComponent(window.location.pathname);
    return;
  }

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return response.json();
}

Fix 5: Correct Your Dev Proxy Config

For Next.js (next.config.js):

javascript
module.exports = {
  async rewrites() {
    return [
      {
        source: "/api/:path*",
        destination: "http://localhost:8000/api/:path*", // backend URL
      },
    ];
  },
};

For Vite (vite.config.js):

javascript
export default {
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:8000",
        changeOrigin: true,
      },
    },
  },
};

For Create React App (package.json):

json
{ "proxy": "http://localhost:8000" }

Quick Debugging Checklist

CheckWhat to look for
Network tab → Status404, 500, 302, or 401 — all indicate a pre-JSON problem
Network tab → ResponseIs it HTML? Paste it into a browser to see what page
Network tab → Content-TypeShould be application/json, not text/html
Request URLIs the /api/ prefix correct? Correct port?
Auth statusHas the session/token expired?
Dev proxyIs the proxy forwarding to the right backend port?

Use JSONKit's JSON Validator to confirm your response payloads are valid JSON before sending them from your API.

Try JSON Validator

Paste your JSON to pinpoint the unexpected token and fix it instantly.