Start with where, not what

A JSON parse error tells you two things: what the parser expected, and where it gave up. The "where" is the useful part. The message is often vague, but the position points at the exact character where the text stopped being valid JSON.

Nearly every parse error comes from one of a short list of causes: a trailing comma, the wrong kind of quote, a comment, an unescaped character inside a string, or a response that was never JSON at all. This guide takes each one in turn, with the real error from Node.js (V8, the engine Chrome also uses) and from Python, then the fix.

Every error message here was produced by running the snippet, not written from memory. Wording shifts between engines and versions, so match on the shape of the message rather than every word.

Reading the error message

Take this object with one stray comma:

{
  "name": "Ada",
  "role": "admin",
}

The two runtimes report it like this:

RuntimeError
Node.js 22SyntaxError: Expected double-quoted property name in JSON at position 38 (line 4 column 1)
Python 3.13JSONDecodeError: Illegal trailing comma before end of object: line 3 column 18 (char 36)
Python 3.12JSONDecodeError: Expecting property name enclosed in double quotes: line 3 column 18 (char 36)

Node points at the closing brace, because that is where it expected another key. Python points at the comma itself. Both are right: the parser reports where it noticed the problem, and the mistake is often just before that spot.

Three things to know when reading these:

  • Position (position 38, char 36) counts characters from 0. Line and column count from 1.
  • Recent Node.js versions and Chrome add the line and column for you. Older ones give only the position, and some errors (Unexpected token ...) give neither.
  • Python 3.13 names trailing commas outright. Python 3.12 and earlier report the same mistake as a missing property name.

If you only have a position, this turns it into a line and column:

function lineAndColumn(text, position) {
    const before = text.slice(0, position);
    return {
        line: before.split('\n').length,
        column: position - before.lastIndexOf('\n')
    };
}

For the example above, lineAndColumn(text, 38) returns { line: 4, column: 1 }, the same spot Node reports.

Trailing commas

The most common parse error is a comma after the last item. JavaScript object literals allow it, so it slips in when you hand-edit a file or copy from source code. JSON does not.

{"roles": ["admin", "engineer",]}
RuntimeError
Node.js 22SyntaxError: Unexpected token ']', ..."engineer",]}" is not valid JSON
Python 3.13JSONDecodeError: Illegal trailing comma before end of array: line 1 column 31 (char 30)
Python 3.12JSONDecodeError: Expecting value: line 1 column 31 (char 30)

Note that Node gives no position at all here, only an echo of the text. In a long document that echo is little help.

Fix: delete the comma before the closing ] or }. If your code builds JSON by joining strings, that is usually where the comma comes from. Build the object and serialize it instead, with JSON.stringify(value, null, 2) or json.dumps(value, indent=2). Neither will ever write a trailing comma.

Single quotes and unquoted keys

JSON strings and keys take double quotes only. Both of these fail at the very first key:

{'name': 'Ada'}
{name: "Ada"}
RuntimeError (same for both)
Node.js 22SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
Python 3.13JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

Unquoted keys usually come from a JavaScript object pasted from source code or the browser console. Single quotes usually come from Python, where str() or print() of a dict writes its repr, not JSON:

>>> d = {"name": "Ada", "active": True, "manager": None}
>>> str(d)
"{'name': 'Ada', 'active': True, 'manager': None}"
>>> json.dumps(d)
'{"name": "Ada", "active": true, "manager": null}'

The repr breaks JSON three ways at once: single quotes, True instead of true, and None instead of null. Fix it at the source by logging or returning json.dumps(d). If all you have is repr text, json.dumps(ast.literal_eval(text)) converts it safely, without running it as code.

Comments, NaN and other JavaScript habits

JSON looks like JavaScript, but it leaves out a lot of what JavaScript allows. Each of these is valid in a .js file and invalid in JSON:

InputNode.js 22Python 3.13
A // comment lineExpected property name or '}' in JSON at position 4 (line 2 column 3)Expecting property name enclosed in double quotes: line 2 column 3 (char 4)
{"score": NaN}Unexpected token 'N', "{"score": NaN}" is not valid JSONParses without error
{"score": undefined}Unexpected token 'u', "{"score": undefined}" is not valid JSONExpecting value: line 1 column 11 (char 10)
{"n": 0x1F}Expected ',' or '}' after property value in JSON at position 7 (line 1 column 8)Expecting ',' delimiter: line 1 column 8 (char 7)
{"n": 007}Unexpected number in JSON at position 7 (line 1 column 8)Expecting ',' delimiter: line 1 column 8 (char 7)

Comments. Files like tsconfig.json and VS Code's settings.json allow comments because their tools read them as JSONC, a looser format. A strict parser rejects them. Remove the comments, or read the file with a JSONC parser such as Microsoft's jsonc-parser package.

NaN is the trap. Python's json module accepts NaN and Infinity by default, and writes them too: json.dumps({"score": float("nan")}) returns {"score": NaN}. A Python service can therefore send a response that every JavaScript client rejects. Pass allow_nan=False to make Python raise ValueError: Out of range float values are not JSON compliant: nan instead, and decide what the value should be (usually null).

Going the other way, JavaScript never writes these values. JSON.stringify({a: NaN, b: undefined, c: Infinity}) returns {"a":null,"c":null}: NaN and Infinity become null, and the undefined key disappears without a warning.

Unescaped characters inside strings

A JSON string cannot contain a raw line break or tab, and every backslash must start a valid escape. These usually appear when text is pasted into a JSON file by hand, or when a string is built by concatenation.

InputNode.js 22Python 3.13
A real line break inside "line one ... line two"Bad control character in string literal in JSON at position 18 (line 1 column 19)Invalid control character at: line 1 column 19 (char 18)
A real tab inside a stringBad control character in string literal in JSON at position 11 (line 1 column 12)Invalid control character at: line 1 column 12 (char 11)
{"path": "C:\Users\ada"}Bad escaped character in JSON at position 13 (line 1 column 14)Invalid \escape: line 1 column 13 (char 12)

Fix: write line breaks as \n, tabs as \t, and each backslash as \\. A Windows path becomes "C:\\Users\\ada", or use forward slashes, which Windows also accepts.

The dangerous case is the one that does not fail. {"path": "C:\new"} parses without any error in both runtimes, because \n is a valid escape. The value comes back as C:, a line break, then ew: five characters, and a broken path. If a path or a regular expression comes out of JSON shorter than it went in, check its backslashes first.

When the response was never JSON

Some parse errors have nothing to do with syntax. The text you parsed is empty, cut off, an HTML page, or several documents in a row.

What you receivedNode.js 22Python 3.13
An empty bodyUnexpected end of JSON inputExpecting value: line 1 column 1 (char 0)
A body cut off mid-objectExpected ',' or '}' after property value in JSON at position 23 (line 1 column 24)Expecting ',' delimiter: line 1 column 24 (char 23)
An HTML error pageUnexpected token '<', "<!DOCTYPE "... is not valid JSONExpecting value: line 1 column 1 (char 0)
Two objects, one per lineUnexpected non-whitespace character after JSON at position 9 (line 2 column 1)Extra data: line 2 column 1 (char 9)

Unexpected token '<' is the classic. The server returned an HTML page, usually a 404, a 500, a login redirect or a proxy error, and your code parsed it as JSON. fetch's response.json() throws exactly this error. Check the status and content type before parsing, and log the start of the body when they are wrong:

const res = await fetch(url);
const text = await res.text();
const type = res.headers.get('content-type') || '';
if (!res.ok || !type.includes('json')) {
    throw new Error(`Expected JSON, got ${res.status} ${type}: ${text.slice(0, 200)}`);
}
const data = JSON.parse(text);

An empty body often means a 204 No Content response, or a request that failed before any body arrived. Calling response.json() on it throws Unexpected end of JSON input.

A cut-off body shows up as an error whose position equals the length of the text. The parser simply ran out of input. Look for a timeout, a size limit, or a log line truncated at a fixed length.

One object per line is JSON Lines (NDJSON), common in logs and data exports. It is not one JSON document, so parse each non-empty line on its own:

const records = text.split('\n').filter(Boolean).map(line => JSON.parse(line));

Invisible characters

The hardest errors to spot are characters that look right on screen. The JSON reads perfectly, and the parser still rejects it.

CharacterWhere it comes fromNode.js 22Python 3.13
Byte order mark (U+FEFF) at the startSome Windows editors and export tools save UTF-8 with a BOMUnexpected token '', "{"name": "Ada"}" is not valid JSONUnexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)
Curly quotes (U+201C, U+201D)Text pasted from Word, Slack, Notion, or anything with smart punctuationExpected property name or '}' in JSON at position 1 (line 1 column 2)Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
Non-breaking space (U+00A0)Examples copied from web pages and documentationUnexpected token ' ', "{"name": "Ada"}" is not valid JSONExpecting value: line 1 column 9 (char 8)

Node's BOM error shows an empty pair of quotes, and its non-breaking space error shows what looks like an ordinary space. When an error points at a character that seems fine, assume it is one of these.

This lists every character outside plain ASCII, with its position and code point:

function findOddChars(text) {
    for (const m of text.matchAll(/[^\x20-\x7E\n\r\t]/g)) {
        console.log(m.index, 'U+' + m[0].codePointAt(0).toString(16).toUpperCase().padStart(4, '0'));
    }
}

For {“name”: “Ada”} it prints positions 1, 6, 9 and 13 with U+201C and U+201D. Accented letters and emoji inside string values will show up too. Those are valid, so only the characters outside strings matter.

Fixes:

  • BOM: in Python, open the file with encoding="utf-8-sig". In JavaScript, text.replace(/^\uFEFF/, '').
  • Curly quotes: replace “ and ” with ". Check first that none of your string values are meant to contain them.
  • Non-breaking spaces: text.replace(/\u00A0/g, ' ').

A checklist, in order of likelihood

When a document will not parse, check these in order:

  1. A comma before a closing } or ]
  2. Single quotes, unquoted keys, or Python's True, False and None
  3. Comments, NaN, undefined, hex numbers or leading zeros
  4. Raw line breaks, tabs or lone backslashes inside strings
  5. An empty, cut-off or HTML body: check the status code and content type
  6. A byte order mark, curly quotes or non-breaking spaces

The fastest way to find the spot is to paste the document into JSON Keyper and click Extract Keys. It shows the line and column of the error, even in browsers whose own message gives only a character position. When the engine gives no position at all, as with the trailing comma in an array above, it finds the comma itself: Trailing comma before "]" (line 1, column 31). JSON does not allow a comma after the last item. The JSON you paste is checked in your browser and never uploaded.

Once the document parses, the next question is whether it has the right fields. That is a job for a schema, covered in JSON Schema: A Practical Guide to Validating JSON.

Find the Exact Line of a JSON Error

Paste the document and get the line and column of any parse error, including trailing commas the browser cannot place. Free, in your browser, no login.

Open the JSON Key Extractor