Unexpected Token in JSON: Every Cause and Fix

Updated 28 Aug 2026 · 9 min read

SyntaxError: Unexpected token < in JSON at position 0 is one of the most-searched error strings in web development, and one of the least helpful. The message tells you where the parser gave up, but the single character it names — a <, a u, a stray quote — almost never tells you why the JSON is broken. This guide covers every real-world cause, the exact browser message each one produces, and the fix, with a live, prefilled JSON Lab example for each so you can see the parser fail on the actual broken input instead of a description of it.

Quick answer: the character named in the error is your best clue. < means you received HTML instead of JSON (a 404 page, a login redirect, a server error page). u almost always means the literal string "undefined" was parsed. A quote, brace, or letter later in the string usually means the JSON itself has a syntax mistake — single quotes, a trailing comma, or an unquoted key.

Quick reference

Messages below are what current Chrome, Edge, and Node.js (all V8) actually print — verified by pasting each example into JSON Lab. Firefox and Safari phrase some of these differently, but the underlying cause and fix are the same everywhere.

What V8 printsMost likely causeFix
Unexpected token '<' ... is not valid JSONServer returned an HTML error or login pageCheck the HTTP status and Content-Type before parsing
Unexpected token '๏ปฟ' ... is not valid JSONUTF-8 byte order mark (BOM) at the start of the fileStrip the BOM before parsing
Expected property name or '}' in JSONSingle-quoted or unquoted object keyWrap every key in double quotes
Expected double-quoted property name in JSONTrailing comma before a closing braceRemove the comma before the closing bracket
"undefined" is not valid JSONThe literal string "undefined" was parsedGuard against undefined before calling JSON.parse
Unexpected non-whitespace character after JSONTwo or more JSON values with no separator (NDJSON if newline-delimited)Split on newlines if NDJSON; otherwise insert commas and wrap in an array

1. An HTML page instead of JSON

By far the most common cause. Your code calls JSON.parse() on the body of an HTTP response, but the server didn't return JSON at all — it returned an HTML document. Every HTML document starts with <!DOCTYPE html> or <html>, and the parser fails on that very first <.

fetch('/api/user')
  .then(res => res.json())   // throws if res isn't actually JSON
  .then(data => console.log(data));

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

This happens when the endpoint 404s and the server's default 404 page is HTML, when an expired session redirects to an HTML login page instead of returning a 401, or when an unhandled server exception returns a framework's HTML error page. In every case, the fix is the same: check the response before you trust it's JSON.

const res = await fetch('/api/user');
if (!res.ok) {
  throw new Error(`Request failed: ${res.status}`);
}
const contentType = res.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
  throw new Error(`Expected JSON, got ${contentType}`);
}
const data = await res.json();

See it fail live

This is the exact payload — a bare 404 page — that produces the error above.

Open this example in JSON Lab

2. A byte order mark (BOM) at position 0

Some editors, and some Windows tools, prepend a UTF-8 byte order mark (U+FEFF) to text files. It's invisible in most editors, but to JSON.parse it's a real, non-whitespace character sitting before the opening { — so the file looks like it starts with garbage even though it prints as valid JSON.

// The string below LOOKS like {"status":"ok"} but has
// an invisible U+FEFF as its first character.
JSON.parse(fileContents);
// Unexpected token '๏ปฟ', "๏ปฟ{"status":"..."... is not valid JSON

Strip it before parsing:

function stripBom(text) {
  return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
}
JSON.parse(stripBom(fileContents));

See it fail live

This example has a real BOM byte in front of the { — paste it in and watch JSON Lab report the same invisible-character failure.

Open this example in JSON Lab

3. Single quotes instead of double quotes

JavaScript object literals accept single quotes. JSON does not — the specification requires double quotes for every string and every key, with no exceptions. Copying an object literal straight out of JavaScript source and calling it JSON is the second most common mistake after the HTML case above.

JSON.parse("{'name': 'GetDevUtils', 'active': true}");
// Expected property name or '}' in JSON at position 1 (line 1 column 2)
// Fixed: double quotes throughout
JSON.parse('{"name": "GetDevUtils", "active": true}');

4. A trailing comma

JavaScript tolerates a trailing comma before a closing brace or bracket. JSON doesn't: a comma always has to be followed by another key, so after reading the comma the parser goes looking for the next double-quoted property name — and finds the closing } instead.

JSON.parse('{"name": "GetDevUtils", "active": true,}');
// Expected double-quoted property name in JSON at position 39 (line 1 column 40)
// Fixed: no comma after the last property
JSON.parse('{"name": "GetDevUtils", "active": true}');

This one is easy to introduce by hand-editing a JSON file, or by generating JSON with naive string concatenation that appends , after every field without checking if it's the last one.

5. An unquoted object key

Also valid JavaScript, also invalid JSON. Every key in a JSON object must be a double-quoted string — bare identifiers like name are not allowed as keys.

JSON.parse('{name: "GetDevUtils", active: true}');
// Expected property name or '}' in JSON at position 1 (line 1 column 2)
// Fixed: keys wrapped in double quotes
JSON.parse('{"name": "GetDevUtils", "active": true}');

6. Parsing the string "undefined"

JSON.stringify(undefined) doesn't return the string "undefined" — it returns the actual JavaScript value undefined. The four-character string undefined shows up a different way: localStorage.setItem() coerces whatever you pass it to a string, and String(undefined) is "undefined". Store an undefined variable without checking it first, and you've written the literal text undefined to storage — not nothing, not null, but four bytes that look like valid code and aren't valid JSON.

let currentUser; // never assigned โ€” still undefined
localStorage.setItem('user', currentUser);
// setItem coerced it: the stored value is now the STRING "undefined"

JSON.parse(localStorage.getItem('user'));
// "undefined" is not valid JSON

This is a different failure from a missing key: localStorage.getItem() returns null for a key that was never set, and JSON.parse(null) quietly returns null — no error at all, since null coerces to the valid JSON literal "null". The crash only happens once something has actually written the string "undefined" into storage.

// Fixed: guard against the literal string, not just a falsy value
const cached = localStorage.getItem('user');
const user = cached && cached !== 'undefined' ? JSON.parse(cached) : null;

7. Two JSON values with no separator between them

Log files and streaming APIs often emit multiple JSON objects back to back with nothing separating them — no array wrapper, no commas, sometimes not even a newline. JSON.parse reads the first complete value successfully, then chokes the instant it hits the start of the second one, since a valid JSON document can only contain exactly one top-level value.

JSON.parse('{"id":1}{"id":2}');
// Unexpected non-whitespace character after JSON at position 8 (line 1 column 9)

The fix depends on whether there's a newline between records. If the source is true NDJSON (newline-delimited JSON — one object per line, the common log-file format), split on newlines and parse each line on its own:

// Fixed, when records are separated by newlines
const records = raw
  .split('\n')
  .filter(line => line.trim())
  .map(line => JSON.parse(line));

The example above has no newline at all — splitting on '\n' wouldn't touch it. For flat objects concatenated with nothing between them, insert the missing commas and wrap the result in an array before parsing:

// Fixed, for objects concatenated with no separator at all
const records = JSON.parse('[' + raw.replace(/\}\s*\{/g, '},{') + ']');

That regex trick only works when the records themselves are flat enough that }{ never appears inside a nested value or a string. For deeply nested or untrusted input, use a real streaming JSON parser instead of a string hack.

Debugging checklist

  1. Log the HTTP status code and Content-Type header before you trust a fetch response is JSON.
  2. Log the raw string's first 20 characters and its length — an HTML page, a BOM, and an empty string all look different here even though the error message alone can't tell them apart.
  3. Paste the exact raw string into a formatter with a real parser and tree view, not a regex-based "JSON-ish" checker — you want the same engine and the same error your production code hits.
  4. If the source is a log file or a stream, check for multiple concatenated objects before assuming the JSON itself is malformed.

๐Ÿงช Paste your broken JSON and see the real error

JSON Lab runs the same parser your browser does — formatting, validation and every example above happen entirely client-side, with nothing uploaded. The one exception is AI Repair: choosing it sends your current input to Gemini to generate a suggested fix, so skip that button for anything sensitive and stick to manual fixes from this guide instead.

Open JSON Lab โ†’

โ† Back to Blog · See also: Bad Control Character in JSON String โ†’