Just Formatter
← Back to blog

json

How to Fix 'Unexpected token < in JSON at position 0'

2026-05-278 min read
Try it now — JSON Formatter & ValidatorOpen full screen →

What this error actually means

SyntaxError: Unexpected token '<' in JSON at position 0 means your code called JSON.parse — or the fetch Response.json() method — on a string whose very first character is an angle bracket. JSON is strict: a valid JSON value must begin with {, [, ", a digit, t (true), f (false), or n (null). An opening angle bracket signals HTML, and the parser immediately rejects it.

The position 0 in the message is the giveaway. It is not a malformed JSON problem midway through a document, and it is not a character encoding issue. The server returned an HTML page instead of JSON data. Understanding that distinction is the fastest path to the fix.

The < character is the first character of every HTML document (<html> or <!DOCTYPE html>). When you see 'position 0', the server replied with an entire HTML page instead of any JSON at all.

Why servers send HTML instead of JSON

The most common cause is an HTTP error response. When a server encounters an unhandled exception, a missing route (404), an authentication failure (401 or 403), or a rate limit breach (429), many web frameworks respond with a styled HTML error page. If your code calls .json() on that response without first checking the HTTP status, you get Unexpected token <.

A second common cause is a reverse proxy intercepting the request. Nginx, Caddy, and Cloudflare all have their own error pages. If your API server goes down or an upstream timeout fires, the proxy returns its HTML error page while your client code still expects JSON.

  • HTTP error pages (401, 403, 404, 429, 500) — the server returns its own HTML error template
  • Reverse proxy error pages — nginx, Caddy, or Cloudflare intercepts when the upstream is down
  • Wrong URL prefix — request hits the React/Next.js router instead of /api handler
  • Redirect to login page — fetch follows the redirect and receives HTML
  • CDN or WAF blocking the request — returns its own HTML access-denied page
  • Dev proxy misconfiguration — Vite or Next.js dev server rewrites to the wrong target

Diagnosing with browser DevTools

Open Chrome or Firefox DevTools with F12 and go to the Network tab. Reproduce the failing request. Click the request and check three things: the Status column, the Content-Type response header under Headers, and the actual body under Preview or Response.

If Preview shows an HTML page, you have found the root cause. Scroll through the HTML to read the error message — it usually identifies the problem clearly: 'Cannot GET /api/users', 'You must be logged in', or a server stack trace. That message is far more useful than the generic Unexpected token < error your JavaScript surfaces.

In the Headers tab, look for Content-Type in the response headers section. A correct JSON API response has Content-Type: application/json. An HTML error page has Content-Type: text/html. This mismatch is your definitive evidence.

The safe fetch pattern

The native fetch API resolves successfully for any HTTP status code — a 500 or 404 response is not thrown as an error. You must check response.ok or response.status before calling .json(). Most developers skip this check, which is why this error is so common.

safeFetch.js
// ❌ Unsafe — throws "Unexpected token <" if server returns an error page
const response = await fetch('/api/users');
const data = await response.json(); // explodes when status is 401/404/500

// ✅ Safe — validate before parsing
async function safeFetch(url, options) {
  const response = await fetch(url, options);

  if (!response.ok) {
    // Read as text so you see the actual error page content
    const body = await response.text();
    throw new Error(
      `HTTP ${response.status} ${response.statusText} — ${body.slice(0, 300)}`
    );
  }

  const contentType = response.headers.get('content-type') ?? '';
  if (!contentType.includes('application/json')) {
    const body = await response.text();
    throw new Error(
      `Expected JSON but received ${contentType}. Body: ${body.slice(0, 300)}`
    );
  }

  return response.json();
}

The double check is important: first verify the HTTP status is successful with response.ok, then verify the Content-Type header includes application/json. Reading the body as text on failure gives you the real server error message, not the cryptic token error.

Fixing it in Axios

Axios automatically throws an error for non-2xx responses, so the status check is handled. However, Axios does not validate the Content-Type header by default, and some proxies return HTML with an incorrect or missing Content-Type. A response interceptor handles both cases.

axiosSetup.js
import axios from 'axios';

axios.interceptors.response.use(
  (response) => {
    const contentType = response.headers['content-type'] ?? '';
    const isDownload =
      response.config.responseType === 'blob' ||
      response.config.responseType === 'arraybuffer';

    if (!isDownload && !contentType.includes('application/json')) {
      throw new Error(
        `Expected JSON but received: ${contentType} — URL: ${response.config.url}`
      );
    }
    return response;
  },
  (error) => {
    if (error.response) {
      const data = error.response.data;
      const msg = typeof data === 'string' ? data.slice(0, 400) : JSON.stringify(data);
      throw new Error(`HTTP ${error.response.status}: ${msg}`);
    }
    return Promise.reject(error);
  }
);

Register this interceptor once at application startup, before any requests are made. From that point, any HTML response throws a clear error identifying the URL and the content type received — no more silent Unexpected token < failures buried in catch blocks.

Fixing the server side in Express and Next.js

If you control the API server, ensure all routes respond with JSON, including error cases. Express has no built-in JSON error handler — unhandled exceptions bubble up to the default HTML error page unless you add a JSON error middleware as the last app.use() call.

expressErrorMiddleware.js
// Express: JSON error handler — register AFTER all routes
app.use((err, req, res, next) => {
  const isApiRoute = req.path.startsWith('/api/');
  const acceptsJson = req.headers.accept?.includes('application/json');

  if (isApiRoute || acceptsJson) {
    return res.status(err.status ?? 500).json({
      error: err.message ?? 'Internal Server Error',
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
    });
  }
  next(err); // let Express render its HTML page for non-API routes
});

// Catch unmatched API routes with a JSON 404
app.use('/api/*', (req, res) => {
  res.status(404).json({ error: `Not found: ${req.method} ${req.path}` });
});

For Next.js App Router API routes, always return NextResponse.json() with an appropriate status in every code path. Never let an API route handler throw an unhandled error to the framework — that produces an HTML 500 page that confuses client-side fetch code.

Using Just Formatter to inspect the raw response

When you cannot open DevTools — headless scripts, CI logs, or server-side rendering — copy the raw response body from your error output and paste it into Just Formatter. If the formatter shows a red error on line 1 and the preview starts with <!DOCTYPE html> or <html>, you have confirmed the server is returning an HTML page.

The formatter's validation shows the exact character that failed and highlights the position, which is far faster than reading an HTML string in a log file. It also lets you quickly confirm whether a response that looks like JSON actually is valid JSON, before you trust it in production code.

💡

Paste the raw response body into Just Formatter's JSON Formatter. If it starts with <!DOCTYPE or <html, your server is returning an error page. Read that HTML to find the real error message — it will tell you exactly what went wrong.

Quick checklist when the error appears

When this error surfaces in production or during development, work through this list in order to find the cause quickly:

  1. Open Network tab in DevTools → find the failing request → check the Status code
  2. Click Response or Preview → confirm the body is HTML, not JSON
  3. Check Content-Type response header — should be application/json
  4. Read the HTML error page content to understand the actual server error
  5. Add response.ok check and Content-Type check to your fetch wrapper
  6. On the server, add a JSON error middleware so all API errors return JSON
  7. Paste the raw body into Just Formatter to validate and inspect quickly