Few error messages waste more developer time than SyntaxError: Unexpected token < in JSON at position 0. It looks cryptic, it shows up in the browser console at the worst possible moment, and the message rarely points at the real problem. The good news: almost every unexpected token in JSON error comes from one of a small handful of causes, and once you can read the message you can usually fix it in seconds.

This guide walks through what a JSON parse error actually means, how to decode the position and token it reports, and the most common reasons JSON.parse() blows up — each with a worked example and a concrete fix. By the end you will be able to glance at a SyntaxError and know exactly where to look.

What 'Unexpected token in JSON' actually means

A JSON parser reads your text character by character, left to right, expecting a strict grammar. When it hits a character that cannot legally appear at that spot, it stops and throws a SyntaxError. The message has three useful parts:

  • The token — the exact character that broke parsing (for example <, o, or }).
  • The position — the zero-based index into the string where parsing failed.
  • The context — newer engines add a snippet like ...is not valid JSON or show the offending line.

The single most important insight is this: the error is almost never a problem with JSON itself. It is a problem with what you handed to the parser. Most of the time you did not pass it JSON at all. Understanding the difference between text, a parsed object, and an HTML response is half the battle. If you want a refresher on the data model first, see our plain-English guide to JSON.

Reading the position and token

The reported position is your map. Position 0 means the very first character was already wrong — the parser never got started, which strongly implies the input is not JSON at all. A position deep in the string means the JSON is mostly valid but has a flaw further in, like a trailing comma or an unquoted key.

Here is a quick lookup table for the tokens you will see most often:

Error fragmentMost likely causeFirst thing to check
Unexpected token < ... position 0You got HTML, not JSON (an error page, login redirect, or 404)Log the raw response body
Unexpected token o ... position 1You parsed an object that was already a JavaScript objectRemove the extra JSON.parse
Unexpected end of JSON inputEmpty string or truncated responseCheck the body is non-empty
Unexpected token } / ]Trailing comma before the closing bracketRemove the dangling comma
Unexpected token ' Single quotes used instead of double quotesReplace ' with "
Unexpected non-whitespace character after JSONExtra data, doubled response, or concatenated objectsInspect the tail of the string

Cause 1: You got HTML instead of JSON

This is the classic Unexpected token < in JSON at position 0. The < is the opening of an HTML tag, usually <!DOCTYPE html> or <html>. Your fetch call hit an endpoint that returned a web page instead of data — a 404 page, a 500 error page, a login redirect, or a proxy notice.

The reason it surprises people is that response.json() happily tries to parse whatever came back, even a 500 status. The fix is to look before you parse:

  • Check response.ok and response.status before calling .json().
  • Log the raw text first: read await response.text() and print it. If it starts with <, you have your answer.
  • Verify the request URL is correct — a wrong path or a relative URL resolving against the wrong base is the usual culprit.
  • Confirm the server actually sets Content-Type: application/json.

A defensive fetch wrapper looks like this in plain terms: send the request, throw a clear error if the status is not OK, read the body as text, and only then parse it — so a bad response gives you a readable message instead of a mystery token. When you do parse manually, wrap JSON.parse in a try/catch and include the first 200 characters of the body in your error log.

Cause 2: 'Unexpected token o' — double parsing

If you see Unexpected token o in JSON at position 1, you almost certainly called JSON.parse() on something that was already an object. When JavaScript converts an object to a string implicitly, it becomes [object Object] — and the parser chokes on the o at position 1.

This happens when you parse a response that was already parsed for you. For example, response.json() already returns a parsed object, so wrapping it again like JSON.parse(await response.json()) is a double parse. The fix is simply to remove the redundant JSON.parse. As a rule: parse a string exactly once, and never parse a value you did not personally receive as text.

Cause 3: Trailing commas and comments

JSON is stricter than JavaScript object literals. Two habits carry over from JS and break parsing every time:

Trailing commas

This is invalid: {"a": 1, "b": 2,}. The comma after 2 tells the parser to expect another key, but it finds } instead — hence Unexpected token }. The same applies to arrays: [1, 2, 3,] fails on the closing bracket. Remove the dangling comma.

Comments

Standard JSON does not allow // or /* */ comments. A comment produces an unexpected token at the slash. If you need comments in a config file, use a format that supports them (like JSON5 or YAML) and convert, or strip the comments before parsing. We cover the trade-offs in JSON vs XML vs YAML.

Cause 4: Quoting and key mistakes

JSON has rigid rules about quotes that trip up developers coming from JavaScript or Python:

  • Keys must be double-quoted. {name: "Ada"} is a JS object literal but not valid JSON; it must be {"name": "Ada"}.
  • Strings use double quotes, never single. {'name': 'Ada'} throws on the first single quote. Python developers hit this when they paste a dict repr or use str(obj) instead of json.dumps(obj).
  • Special characters inside strings must be escaped. A literal newline, tab, or unescaped double quote inside a string breaks parsing. Use \n, \t, and \".
  • Python and JS literals are not JSON. None, True, and False must become null, true, and false. NaN and Infinity are not valid JSON at all.

For the complete grammar — what counts as a valid object, array, key, and value — see our breakdown of the JSON syntax rules, and the round-up of 10 common JSON mistakes that cause these errors in real codebases.

Cause 5: Empty, truncated, or extra data

Two more messages deserve their own note because they are not really about a wrong character:

  • Unexpected end of JSON input means the parser ran out of text before the structure was complete. The usual cause is an empty string (a 204 No Content response, or a body you forgot to await) or a truncated payload from a dropped connection. Guard with a check: if the trimmed text is empty, do not call parse.
  • Unexpected non-whitespace character after JSON means there is valid JSON followed by junk — often a response that was written twice, or two JSON objects concatenated without an array around them. Inspect the tail end of the string.

A repeatable debugging workflow

When a JSON parse error appears, work through this checklist in order rather than guessing:

  1. Print the raw input exactly as the parser received it — console.log(JSON.stringify(rawText)) reveals hidden whitespace, a leading byte-order mark, or HTML at the start.
  2. Read the position. Position 0 means wrong input type; a later position means a structural flaw at that index.
  3. Match the token to the table above and jump straight to the likely cause.
  4. Validate the snippet in a tool. Paste it into a free JSON formatter & validator — it will point at the exact line and character, which is far faster than reading a position index by hand.
  5. Fix at the source. If the server is sending HTML or single-quoted output, fix the producer, not just the consumer.

One more habit worth adopting: never parse without a try/catch in production code, and always include a slice of the offending input in the caught error. A log line like Failed to parse response, body started with: <!DOCTYPE turns a 30-minute investigation into a 30-second one.

Key takeaways

  • The token and position tell you almost everything. < at position 0 means HTML, not JSON; o at position 1 means you double-parsed an object.
  • The problem is usually the input, not your JSON. Most parse errors come from a wrong response, a wrong type, or JS/Python habits leaking into JSON.
  • JSON is strict: double-quoted keys and strings, no trailing commas, no comments, and null/true/false instead of language-specific literals.
  • Build defensive parsing: check response.ok, log the raw body, wrap JSON.parse in try/catch, and validate suspicious payloads in a formatter before you ship.

Master those reflexes and Unexpected token in JSON stops being a mystery and becomes a quick, mechanical fix.