You call JSON.parse() and get a SyntaxError. The message says Unexpected token < at position 0. The JSON is coming from an API. Position 0 means the very first character is wrong.
Most of the time, it is not your JSON at all. It’s an HTML error page.
That is the kind of thing these error messages rarely make obvious. They point you at a character, a position, sometimes a line. But the real problem is usually one level up: what you were given was never JSON to begin with, or the JSON was valid but your pipeline mangled it before it got to JSON.parse().
This post goes through each common SyntaxError variant, what it’s actually telling you, and how to get to the root cause faster. There’s also a section at the end on the performance cost of parsing large JSON on the main thread, which is easy to miss until it shows up as a Long Task in your traces.
The messages and what they mean
Unexpected token < at position 0
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
The response starts with <. That’s HTML. You got an error page, a redirect page, or a server-rendered HTML response where your code expected JSON.
This almost always means one of three things:
- The server returned an error. A 500, a 404, a 401. The body is an HTML error page. Your fetch code checked
response.okbut didn’t guard before callingresponse.json(). - The URL is wrong. You’re hitting a web page, not an API endpoint. Maybe the path changed or you have a typo.
- The server is returning HTML for unauthenticated requests. You’re getting a login page because your auth token expired or wasn’t sent.
The fix is upstream of JSON.parse():
const res = await fetch('/api/data');
if (!res.ok) {
// Read it as text first so you can see what the server actually sent
const body = await res.text();
console.error('Non-OK response:', res.status, body.slice(0, 200));
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
Reading the raw text when the response is not OK tells you immediately whether it’s an HTML error page, a plain-text error message, or something else.
Unexpected end of JSON input
SyntaxError: Unexpected end of JSON input
The parser hit the end of the string before the JSON was complete. The structure was valid up until that point, then it just stopped.
Common causes:
- Truncated response. Network cut out, server timed out, or a CDN cached a partial body. If this happens sporadically, it’s a network or server issue.
- You’re concatenating strings incorrectly. If you’re building JSON by hand (don’t) or appending chunks, you may have missed the closing brackets.
- An empty string was passed to
JSON.parse().JSON.parse("")throws this. Check that the string is non-empty before parsing.
// Fragile
const data = JSON.parse(responseText);
// Safer
if (!responseText || responseText.trim() === '') {
throw new Error('Response was empty');
}
const data = JSON.parse(responseText);
If the input is coming from outside your control and may be partial, JSON repair can reconstruct valid JSON from a truncated or broken payload. It won’t recover lost data, but it can salvage a response that is 95% valid and only broken at the tail.
Expected property name or ’}’ (trailing comma)
SyntaxError: Expected property name or '}' in JSON at position 42
Or in some engines:
SyntaxError: JSON.parse: expected property name or '}' at line 3 column 1
This is almost always a trailing comma. JSON does not allow a comma after the last property in an object or the last item in an array. JavaScript does (since ES2017). This is a real source of confusion because valid JS object literals are not valid JSON.
// Invalid JSON
{
"name": "Alice",
"age": 30,
}
// Valid JSON
{
"name": "Alice",
"age": 30
}
If you’re generating JSON from code, use JSON.stringify() and never build JSON by string concatenation. If the JSON is coming from a config file or a human-written source, paste it into the JSON validator to get the exact line and column, not just a position offset.
The trailing comma fixer will strip all trailing commas from an object or array automatically. Useful when you’re getting JSON back from a non-standard source (some older APIs and internal tooling output JS-style objects, not strict JSON).
Unexpected token , (leading or double comma)
SyntaxError: Unexpected token ','
A comma where one is not expected. Either:
- A double comma:
[1,,2]. Usually a code generation bug or a bad string replace. - A leading comma:
[,1,2,3]. Less common, but shows up when someone prepends items incorrectly. - A comma before a bracket:
{"a": 1,}which is the trailing comma case above.
Paste the broken JSON into the JSON formatter. It will highlight the exact location of the problem and show you the structure around it, which is much faster than counting characters from a position offset.
Unexpected token u / undefined
SyntaxError: Unexpected token 'u', "undefined" is not valid JSON
You passed the string "undefined" to JSON.parse(). This happens when you do something like:
const raw = localStorage.getItem('missing-key'); // returns null
JSON.parse(raw); // SyntaxError because null is passed, or...
const value = someObject.key; // key doesn't exist, value is undefined
JSON.parse(value); // SyntaxError: undefined converted to string "undefined"
JSON.parse(null) returns null (that one actually works). JSON.parse(undefined) converts undefined to the string "undefined" and then throws.
The guard:
const raw = localStorage.getItem('data');
const data = raw ? JSON.parse(raw) : null;
The position number is not what you think
All these errors include a position. Browsers report position differently.
- Chrome/V8 reports a byte offset from the start of the string.
- Firefox/SpiderMonkey reports line and column numbers.
- Safari/JavaScriptCore has its own format.
For compact single-line JSON (the kind you usually get from APIs), the position offset is usable. For pretty-printed JSON, counting to character 3,847 in a 500-line object is not practical.
The reliable approach: take the raw string, paste it into the JSON parse error debugger, and get the highlighted problem location with surrounding context. It runs in your browser, nothing is sent anywhere.
When the JSON is technically valid but structured wrong
A SyntaxError means the parser could not read the JSON at all. A different class of problem is when the JSON parses fine but the shape is not what your code expects.
const data = JSON.parse(response);
console.log(data.user.name); // TypeError: Cannot read properties of undefined
The JSON was valid. But data.user is null, or data is an array where you expected an object, or the key is User not user.
This is not a parse error. It’s a schema mismatch. The right tool here is JSON schema validation, which lets you define exactly what shape you expect and surface every deviation, not just the first one that causes your code to crash.
For API responses you don’t control, validating against a schema before trusting the shape is the production-grade approach. JSON.parse() only tells you if the string is syntactically valid JSON. It says nothing about whether the data inside matches what your application needs.
JSON.parse() runs on the main thread and it blocks
This is the performance angle that is easy to miss until it shows up in a trace.
JSON.parse() is synchronous. It runs to completion on the main thread before anything else can run. For most responses this is fine. For large ones, it is a measurable Long Task.
| Payload size | Approximate parse time (V8, mid-range laptop) |
|---|---|
| 50 KB | under 1ms |
| 500 KB | 5 to 15ms |
| 5 MB | 50 to 200ms |
| 50 MB | 500ms or more |
A 5MB JSON response is a realistic threshold where parsing crosses the 50ms Long Task boundary. If your app fetches large datasets, list responses, or full document payloads, you may be blocking the main thread on every fetch without realising it.
The Chromium team demonstrated in 2023 that parsing cost scales roughly linearly with the size of the JSON string, not the complexity of the structure. A flat array of 50,000 strings can be as expensive as a deeply nested document of the same byte length.
What you can do about it:
1. Parse in a Web Worker.
// worker.js
self.onmessage = (e) => {
const parsed = JSON.parse(e.data);
self.postMessage(parsed);
};
// main thread
const worker = new Worker('/worker.js');
worker.postMessage(rawJsonString);
worker.onmessage = (e) => {
// e.data is the parsed object, transferred off the main thread
};
The parse still happens somewhere, but the main thread is not blocked during it. The tradeoff is the structured clone cost of passing the result back, which matters for very large objects.
2. Validate and reject early.
If you’re fetching JSON that might be large, validate the Content-Length header before you even read the body. A 50MB response you can’t handle is better rejected at the network level than parsed on the main thread.
3. Request only what you need.
Most APIs support pagination, field selection, or filtering. A GraphQL query or a REST endpoint with ?fields=id,name returns a fraction of the data. The best way to make JSON.parse() fast is to give it less to parse.
4. Stream parsing for very large payloads.
JSON.parse() has no streaming API in JavaScript yet. For truly large payloads, oboe.js and @streamparser/json offer streaming JSON parsing that processes the response chunk by chunk as it arrives over the network. Useful if you’re building something like a log viewer or a data export tool.
When you need to fix JSON you didn’t write
Not every JSON problem is in code you control. Sometimes you’re dealing with:
- An export from a tool that outputs almost-valid JSON (trailing commas, unquoted keys, comments).
- A log file or debug dump that has JSON mixed with other text.
- A response from an old internal API that predates strict JSON and uses JS-style objects.
For that class of problem, the JSON repair tool attempts to reconstruct valid JSON from broken input. It handles trailing commas, single quotes instead of double quotes, unquoted keys, Python-style None, True, False values, and truncated payloads.
It is not magic. If the JSON is structurally incoherent, repair can only do so much. But for the common cases, it produces valid output you can actually parse.
Quick reference
| Error message | Most likely cause | Where to look |
|---|---|---|
Unexpected token < | Got HTML, not JSON | Check response status first |
| Unexpected end of input | Truncated response or empty string | Log raw text before parsing |
| Expected property name or ’}‘ | Trailing comma | JSON validator |
| Unexpected token , | Double or leading comma | JSON formatter |
| Unexpected token u | Parsing undefined | Guard for null/undefined before parsing |
| Valid JSON, broken shape | Schema mismatch | JSON schema validator |
The pattern across all of them: read the raw response text first, before calling JSON.parse(). Most of the information you need is in that text, not in the error message the parser gives you.