Unexpected Token in JSON: Every Cause and Fix
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.
< 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 prints | Most likely cause | Fix |
|---|---|---|
Unexpected token '<' ... is not valid JSON | Server returned an HTML error or login page | Check the HTTP status and Content-Type before parsing |
Unexpected token '๏ปฟ' ... is not valid JSON | UTF-8 byte order mark (BOM) at the start of the file | Strip the BOM before parsing |
Expected property name or '}' in JSON | Single-quoted or unquoted object key | Wrap every key in double quotes |
Expected double-quoted property name in JSON | Trailing comma before a closing brace | Remove the comma before the closing bracket |
"undefined" is not valid JSON | The literal string "undefined" was parsed | Guard against undefined before calling JSON.parse |
Unexpected non-whitespace character after JSON | Two 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 Lab2. 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.
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}');
See it fail live
Open this example in JSON Lab4. 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.
See it fail live
Open this example in JSON Lab5. 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}');
See it fail live
Open this example in JSON Lab6. 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;
See it fail live
Open this example in JSON Lab7. 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.
See it fail live
Open this example in JSON LabDebugging checklist
- Log the HTTP status code and
Content-Typeheader before you trust a fetch response is JSON. - 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.
- 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.
- 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 โ