~/blog

JSON.parse error: "Unexpected token at position N" — what it means and how to fix it

published

#json#debugging#javascript

SyntaxError: Unexpected token X in JSON at position N is the error every developer hits within their first month of working with APIs. The message tells you exactly where the parser gave up — but the why is hidden behind one of about five different surface causes. This post decodes the error message, shows the cause-by-shape for the most common variants, and points at the tool that turns the offset into a line and column you can actually use.

What the error literally says

When JSON.parse() fails, the JavaScript engine throws a SyntaxError. The shape of the message depends on which engine is reading your JSON:

EngineExample message
V8 (Chrome, Node) — oldUnexpected token a in JSON at position 12
V8 (Chrome 119+, Node 21+)Unexpected token 'a', "..." is not valid JSON
SpiderMonkey (Firefox)JSON.parse: unexpected character at line 2 column 5 of the JSON data
JavaScriptCore (Safari)JSON Parse error: Unexpected EOF

The position N is a byte offset into the input string. Position 0 is the first character. So position 12 means “the 13th character is where the parser choked”. Modern V8 wraps it in a snippet to help, but the offset is still the load-bearing field.

To convert an offset into a line and column, the JSON formatter and error reviewer does the math for you and highlights the exact spot — paste the broken JSON and watch the snippet show the surrounding characters with a red gutter pointer.

The five causes, in rough order of frequency

1. Unquoted object keys

{ name: "ada" }

JavaScript object literals allow unquoted keys. JSON does not. The first non-whitespace character of an object after { must be a ". The parser sees n and immediately fails — usually position 2 (counting whitespace).

Fix: { "name": "ada" }.

2. Single-quoted strings

{ 'name': 'ada' }

JSON requires double quotes for every string. The parser sees ' and fails. Almost always copy-pasted from a JavaScript file.

Fix: swap every ' for ". Find-and-replace works because legitimate single quotes inside JSON strings are never escaped.

3. Trailing commas

[1, 2, 3,]

Or:

{ "a": 1, "b": 2, }

JavaScript accepts trailing commas in arrays and objects. Strict JSON does not. The parser sees the ] or } after the comma and fails because it expected another value.

Fix: delete the comma. If you’re parsing a file that allows them (e.g. tsconfig.json, VS Code settings, ESLint config), it’s JSONC, not strict JSON. The JSON formatter has a JSONC toggle that strips trailing commas and // / /* */ comments before parsing.

4. Embedded control characters in strings

{ "msg": "line one
line two" }

A literal newline inside a JSON string is illegal. So is a literal tab. They must be escaped: \n, \t. This trips up developers who hand-craft JSON from a multi-line text file.

Fix: escape control characters. JSON.stringify() does it for you — never build JSON by string concatenation.

5. Numbers with leading zeros or trailing dots

{ "id": 007 }

JSON’s number grammar forbids leading zeros (except 0 itself), trailing dots (5.), + prefixes (+1), and non-finite values (Infinity, NaN).

Fix: quote it as a string. { "id": "007" }. If you need it as a number downstream, parse it in your code.

Cross-engine error message normalization

Because the surface shape varies, a robust error-locator implementation needs to:

  1. Try to read position N from the message (V8 + Node).
  2. Fall back to line L column C (Firefox).
  3. Fall back to its own scan when the engine omits the offset (Safari, modern V8 with the short-form message).

The third fallback walks the input one character at a time, tracking whether it’s inside a string, the current nesting depth, and the last valid token. When it finds a character that can’t continue any valid JSON token, that’s the error position. It’s about 50 lines of code — the JSON formatter ships exactly that.

When the error fires after the obvious bug

Two patterns hide the real cause:

Unterminated string

{ "name": "ada }

The parser keeps reading characters into the string until it finds the closing ". If the closing quote is missing, it consumes the rest of the input and reports the error at the end of the file, not where the string started. Look upward from the reported position for the unmatched opening quote.

Mismatched brackets

{ "items": [1, 2, 3 }

Same dynamic: the parser consumes everything until it hits a character that can’t appear inside an array. The position is at the }, but the real bug is the missing ].

A linter that tracks bracket depth catches both. The JSON formatter shows a red gutter at the parser’s reported position but the snippet around it usually includes the unclosed token a few characters up.

When the input is valid JSON but you still get an error

Two surprises:

  1. You’re parsing a Response body twice. await response.json() consumes the body. A second call throws because the stream is empty.
  2. You’re parsing a string that’s already an object. JSON.parse(myObject) calls myObject.toString() first, which yields "[object Object]", which is not valid JSON.

The error message is the same. Logging the type of the input (console.log(typeof x, x?.constructor?.name)) before the parse call diagnoses both in seconds.

Tooling

Both run entirely in your browser; the JSON you paste never leaves the tab.

Takeaway

The error message is precise but the surface shape varies. Read the byte offset, translate to a line and column, then check the five usual causes in order: unquoted keys, single quotes, trailing commas, raw control characters, malformed numbers. Most fixes are one character. The few that aren’t are usually an unterminated string a few lines up from the reported position.