One fault, four unrelated-looking messages
There is no such thing as a universal JSON error message — every JavaScript engine writes its own parser, in its own words, and none of the four major ones agree on phrasing. RFC 8259 defines what is and is not valid JSON with total precision; it says nothing at all about what an implementation must print when input violates it. That silence is why the same broken document produces four differently worded errors depending on where it runs, and why a search for the exact string in front of you can miss pages written around a different engine’s phrasing of an identical underlying mistake:
| Engine | Runs in | Generic wording for this fault |
|---|---|---|
| SpiderMonkey | Firefox | JSON.parse: unexpected character at line L column C of the JSON data |
| V8, current | Chrome, Node ≥ 20, Edge, Deno | Unexpected token 'X', "…"... is not valid JSON |
| V8, older | Chrome < 118, Node < 20 | Unexpected token X in JSON at position N |
| JavaScriptCore | Safari, Bun | JSON Parse error: Unrecognized token 'X' |
Firefox’s unexpected character phrasing is its catch-all: it appears
whenever the very next character cannot begin any legal JSON token in the current context,
which is the same condition Chrome describes as an unexpected token and Safari as an
unrecognised token. This page treats all three as one fault and gives the fix once.
The exact trigger determines the exact wording
Not every invalid character produces the generic message in every engine — some triggers get a more specific message instead, because the parser was already in a grammar position (such as expecting a property name) where it can name what it wanted. The table below is measured directly against current Chromium, Firefox and WebKit, not guessed:
| Input | Chrome/Node (V8) | Firefox | Safari |
|---|---|---|---|
| 'hello' | Unexpected token ''' | unexpected character | Single quotes (') are not allowed in JSON |
| [1,2,] | Unexpected token ']' | unexpected character | Unexpected comma at the end of array expression |
| {"a":1,} | Expected double-quoted property name | expected double-quoted property name | Property name must be a string literal |
| {a:1} | Expected property name or '}' | expected property name or '}' | Expected '}' |
| NaN / Infinity | "NaN" is not valid JSON | unexpected character | Unexpected identifier "NaN" |
| // or /* comment | Unexpected token '/' | unexpected character | Unrecognized token '/' |
| BOM (U+FEFF) first | Unexpected token '�' | unexpected character | Unrecognized token '�' |
| “smart quotes” | Expected property name or '}' | expected property name or '}' | Unrecognized token '“' |
The pattern: Firefox collapses almost everything that is not a specific grammar position into one generic “unexpected character” message, while Chrome and Safari more often name the specific token. If you searched the Firefox wording and landed here, your actual trigger is very likely one of the causes below, worded differently in whatever engine produced the page you are debugging against.
The causes, most common first
1. Single quotes instead of double quotes
{'name': 'Ada'}
{"name": "Ada"}
JSON strings are double-quoted, without exception. This most often arrives from a
JavaScript object literal pasted rather than serialised, or from Python’s
str(dict) / repr() output, which resembles JSON closely enough to
be mistaken for it.
Fix: in Python, use json.dumps(d), never str(d).
In JavaScript, build the value and call JSON.stringify rather than writing the
text by hand.
2. A trailing comma
{
"port": 5432,
}
{
"port": 5432
}
Allowed in JavaScript, Python, Rust and Go object/array literals; never allowed in JSON. This is the single most common hand-editing mistake, because every modern language and linter has trained developers that a trailing comma is harmless.
Fix: delete the comma. If the document is generated, stop building it by string concatenation and serialise a real value instead — a JSON library cannot produce a trailing comma.
3. Unquoted property names
{name: "Ada", age: 36}
{"name": "Ada", "age": 36}
A perfectly valid JavaScript object literal, and invalid JSON — every key must be a double-quoted string, including keys that look like plain identifiers.
4. NaN, Infinity, undefined, None
| Not JSON | Usually comes from | Write instead |
|---|---|---|
| NaN | A float division gone wrong upstream | null |
| Infinity | Overflow or a deliberate sentinel | null |
| undefined | JavaScript string concatenation | null |
| None, True, False | Python’s str(dict) | null, true, false |
JSON has no representation for not-a-number or infinity. Python’s
json.dumps will emit NaN by default unless you pass
allow_nan=False, producing a file that no strict JSON parser, including every
browser’s, can read back.
5. A byte order mark
A UTF-8 BOM — the invisible character U+FEFF, written by Notepad, PowerShell redirection and Excel’s export by default — is legal inside a UTF-8 file and rejected by every JSON parser when it appears at the very start, because it is not whitespace as far as the JSON grammar is concerned.
# Python — the "sig" codec strips a BOM if present, and is harmless if absent
data = json.load(open('file.json', encoding='utf-8-sig'))
# JavaScript
if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1);
# Shell — check for one
head -c 3 file.json | xxd # "efbbbf" confirms a BOM
6. Comments
JSON has no comment syntax; neither // nor /* */ is legal
anywhere in the document. If you control the format and want comments, JSON5, JSONC or YAML
exist for exactly this reason — fighting JSON’s strictness in a hand-edited file
is a losing game.
7. Smart quotes pasted from a document
Word, Google Docs, Slack and iOS all substitute curly quotes
(“ U+201C, ” U+201D) for straight ones automatically.
They render close enough to " to be invisible on screen, and every JSON parser
rejects them. This is one of the few triggers where the visible text and the actual bytes
genuinely disagree — the fix is retyping the quotes rather than reading harder.
8. A missing comma between values
[1 2 3]
[1, 2, 3]
Every array element and every object member must be separated by exactly one comma — not whitespace alone. This is a common artefact of hand-editing a long array and forgetting a separator when adding a line.
Worked example
A configuration file edited by hand in a rich-text-aware editor picks up curly quotes without anyone noticing:
{“timeout”: 30, “retries”: 3}
In Firefox this throws JSON.parse: expected property name or '}' at line 1 column 2
of the JSON data — not the generic “unexpected character” wording,
because the parser is specifically expecting a property name at that position and can name
it. In Chrome/Node it is Expected property name or '}' in JSON at position 1.
Pasting it into the tool above marks the exact curly quote and names it, which on screen
looks identical to a straight one:
{"timeout": 30, "retries": 3}
What to check, in order
- Paste the document above; the caret and code-point name settle ambiguous-looking characters (curly quotes, BOM, non-breaking space) in seconds.
- Check whether the document was ever opened or edited in a word processor, chat client or PDF — the most common source of invisible substitutions.
- Check whether it was generated by string concatenation, a template, or a language
other than the one that will parse it (Python’s
repr/stroutput being the classic case). - Run
head -c 3 file.json | xxdif position 0/column 1 is implicated and the file looks fine to the eye.
Why Firefox collapses so many triggers into one message
SpiderMonkey’s parser reports a specific expectation — a property name, a
comma, a closing bracket — whenever it can name one precisely for the grammar position
it is in. “Unexpected character” is what remains once none of those more precise
messages apply: the parser was simply looking for the start of any valid JSON value or token
and found something that cannot begin one, with nothing more specific to say about what it
expected instead. That is why a bare NaN, a bare single-quoted string, a leading
BOM and an opening /* comment all collapse into the identical wording in
Firefox even though a human reader would describe them as four unrelated mistakes —
structurally, to the parser, they are the same situation: an illegal character where a value
was expected, and no narrower category to report.
Quick reference
| What you see | Most likely cause |
|---|---|
| unexpected character … column 1 | Something invalid at the very start — BOM, comment, bare NaN/Infinity, single quote |
| expected property name or '}' | Unquoted key, or a smart quote where a key should start |
| expected double-quoted property name | Trailing comma before a closing brace |
| Single quotes (') are not allowed in JSON | Safari's specific wording for cause 1 |
| Unexpected comma at the end of array expression | Safari's specific wording for a trailing comma in an array |
| Unrecognized token 'X' | Safari's generic catch-all, roughly Firefox's "unexpected character" |
Limitations and edge cases
- Firefox’s generic wording can mean several different underlying
triggers — single quotes, a bare
NaN, a BOM, a comment, and a curly-quote-in-a-bare-string-context can all surface as “unexpected character.” The table above narrows it down by what you actually typed, but the tool’s own diagnosis is the fastest way to be certain. - Column numbers count differently across engines. Firefox and Safari report line and column; current Chrome/Node report both a character offset and a line/column; older Chrome/Node report only the character offset. All three are counting the same document, just presenting the position differently.
- Bun uses JavaScriptCore, so its
JSON.parsemessages match Safari’s column in the table above, not V8’s, even though Bun is a server-side runtime most people associate with Node. - Deno and current Node share V8 with Chrome, so their wording tracks the “V8, current” row exactly, including across Node version upgrades that bump the underlying V8 release.
Frequently asked questions
Why does the same broken JSON produce different error text in different browsers?
Because JSON.parse is specified by behaviour —
what is valid and what result a successful parse produces — not by the exact wording
of a failure. Chrome, Firefox and Safari each ship their own independently written JSON
parser (V8, SpiderMonkey and JavaScriptCore respectively), and each is free to phrase a
rejection however its authors chose to.
Is Firefox’s message less informative than Chrome’s?
Not in general — Firefox reports a line and column for every failure, which Chrome only added in its 2023 message overhaul. Firefox’s “unexpected character” wording is simply more generic in the specific cases where it cannot name a more precise grammar expectation, which is a design choice, not a capability gap.
My error says “JSON Parse error” with no further detail at all. Which engine is that?
JavaScriptCore — Safari on any platform, or Bun. Several of its messages are noticeably terser than the other three engines’, sometimes omitting a position entirely. Paste the document above for a caret and code-point name regardless of which engine produced the original error.
Does Node always match Chrome’s wording?
Yes when both are on a current version — they share the V8 engine. An older Node release paired with a current Chrome (or vice versa) can disagree, because V8’s JSON error messages changed substantially in the 2023 release that added line/column reporting. Check both versions if the wording looks unfamiliar.
Can I make my own code produce one consistent message regardless of browser?
Yes — write your own thin wrapper that catches whatever
JSON.parse throws and rethrows a message you control, including the input
snippet and your own position-finding logic rather than relying on the native message text
at all. That is the same approach this page’s embedded tool takes.
Is a curly quote really that hard to spot?
Yes, deliberately so — that is why word processors and chat
apps substitute it automatically, to look better typeset. At most font sizes and in most
editors “ and " are close to indistinguishable on screen.
The tool above names the exact Unicode code point, which ends the guessing.
Is it safe to paste a failing production payload into this page?
Into this one, yes: there is no upload endpoint, no storage and no share link, and every check runs in your tab. That is not true of hosted formatters generally — in November 2025 two of the most widely used online code-formatting utilities were found to have been exposing years of stored submissions, credentials included. The details, with sources.
Why does Firefox say “unexpected character” for so many different mistakes?
Because it is SpiderMonkey’s fallback wording for any
position where the parser is looking for the start of a legal JSON value or token and
finds something that cannot begin one, with no narrower grammar expectation available to
name. A bare NaN, a leading BOM, a single-quoted string and an opening
comment are different mistakes to a human but the identical situation to the parser
— an illegal character where a value belonged — which is why they all collapse
into one message. Chrome and Safari draw that line in slightly different places, which is
why the same input can look more specific in one and more generic in another.
Related pages
- “Unexpected token” explainedEvery cause of the most common JSON error
- “Unexpected token < … position 0”Your API returned HTML, not JSON
- “Unexpected end of JSON input”Empty bodies, truncation and double-reads
- JSON validatorCheck syntax and get the exact line and column