Published 1 October 2026 · 9 min read
JSON.parse Unexpected Character at Line 1 Column 1: Fixes
Firefox's version of the classic parse failure is SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data. It looks like a complaint about your JSON. It almost never is. Column 1 means the very first character was already wrong — the string you handed to JSON.parse didn't start like JSON at all, so the parser gave up before it read a single key. Almost every fix starts with finding out what that string really is.
await res.text() before parsing. If it begins with < you got an HTML page, if it begins with a word (Internal, Not, undefined) you got plain text or a stringified undefined, and if it begins with )]}' or a function name you got an anti-hijacking prefix or JSONP. Each case below has a live, prefilled example.
The same failure, as each engine words it
The V8 column was produced by running each input through JSON.parse in Node 22. Firefox's message format is unexpected character at line L column C of the JSON data, with the line and column of the first rejected character (I could only run V8 here, so the Firefox column is derived from that character's position rather than copied from a Firefox console).
| Input | V8 (Chrome, Edge, Node) | Firefox column |
|---|---|---|
<!DOCTYPE html>… | Unexpected token '<', "<!DOCTYPE "... is not valid JSON | 1 |
Internal Server Error | Unexpected token 'I', "Internal S"... is not valid JSON | 1 |
undefined | "undefined" is not valid JSON | 1 |
callback({"a":1}) | Unexpected token 'c', "callback({"a":1})" is not valid JSON | 1 |
{"score": NaN} | Unexpected token 'N', "{"score": NaN}" is not valid JSON | 11 |
{'a':1} | Expected property name or '}' in JSON at position 1 (line 1 column 2) | 2, and a different message (expected property name) |
Two things to notice. First, V8 shortens some of these to "undefined" is not valid JSON with no character named, so the search you run in Chrome and the search you run in Firefox won't match even though the bug is identical. Second, the last two rows show the column earning its keep: NaN fails at column 11, and single-quoted keys fail at column 2 with a different wording entirely. If you see column 1, look at the start of the string, not the middle.
1. An HTML page where JSON should be
By far the most common cause. A 404 from a wrong URL, a 500 from your framework's error page, a login redirect, or a CDN challenge page all return HTML, and res.json() hands the first < to the parser.
const res = await fetch('/api/usres'); // typo: the route doesn't exist
const data = await res.json();
// Firefox: JSON.parse: unexpected character at line 1 column 1 of the JSON data
// Chrome: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
The fix is to look at the response before trusting it. This wrapper turns the cryptic parse error into one that names the URL, status and content type:
async function fetchJson(url, options) {
const res = await fetch(url, options);
const text = await res.text();
const type = res.headers.get('content-type') || '';
if (!res.ok || !type.includes('json')) {
throw new Error(`Expected JSON from ${url}, got ${res.status} ${type || 'no content-type'}: ${text.slice(0, 120)}`);
}
return JSON.parse(text);
}
One caveat: a few servers send valid JSON as text/plain. If yours does, drop the content-type test and keep the status check, or fix the server.
See it fail live
A minimal 404 page, pasted as if it were an API response. The parser rejects the first character.
Open this example in JSON Lab2. A plain-text error message
Rate limiters, load balancers and hand-rolled handlers often reply with a bare string: Internal Server Error, Too Many Requests, Bad Gateway. It is not JSON, and it doesn't need to be an HTML page to break your client.
const res = await fetch('/api/report');
const report = await res.json(); // body is: Too Many Requests
// unexpected character at line 1 column 1 of the JSON data
The same guard as above catches this. The real fix is upstream: have the API return a JSON error body ({"error":"rate_limited"}) with a matching status code, so clients never have to guess.
See it fail live
Open this example in JSON Lab3. Parsing the value undefined
JSON.parse(undefined) converts its argument to a string first, so the parser really does receive the text undefined — and u is not a legal way to start a JSON value. This is what a missing localStorage key or an absent environment variable looks like after a JSON.parse call with no guard.
const saved = JSON.parse(sessionStorage.getItem('filters') ?? undefined);
// "undefined" is not valid JSON (Firefox: unexpected character at line 1 column 1)
// Fixed: only parse what exists
const raw = sessionStorage.getItem('filters');
const saved = raw ? JSON.parse(raw) : {};
See it fail live
Open this example in JSON Lab4. An anti-hijacking prefix ()]}')
Some APIs deliberately prepend a non-JSON line to every response so it can't be loaded as a script by another site. AngularJS documents )]}', as one such prefix, and Google's older JSON endpoints use similar guards. A client that doesn't know to strip it fails at column 1 every time.
const text = await res.text();
// body: )]}',\n{"user":"ada"}
const data = JSON.parse(text.replace(/^\)\]\}',?\n/, ''));
See it fail live
Paste the body with its prefix line and the parser stops at the ). Delete that first line and the same payload validates.
5. JSONP instead of JSON
If the URL accepts a callback parameter, the server may wrap the payload in a function call: callback({"user":"ada"}). That is JavaScript, not JSON, and it fails at the first letter. Remove the callback= parameter, or strip the wrapper by hand.
See it fail live
Open this example in JSON Lab6. Not always column 1: Python's NaN
Python's json.dumps happily writes NaN, Infinity and -Infinity by default, none of which exist in JSON. The document starts fine, so the error points deep inside it — column 11 for the sample below. This is the case to remember when a column number is greater than 1: the parser got somewhere before it hit a bad literal.
# Python side
json.dumps({"score": float("nan")}) # '{"score": NaN}' -- not valid JSON
json.dumps({"score": float("nan")}, allow_nan=False) # raises ValueError instead
Pass allow_nan=False so the server fails loudly, or replace non-finite numbers with None (JSON null) before serializing.
See it fail live
Open this example in JSON LabDebugging checklist
- Replace
res.json()withres.text()and log the first 200 characters, the status code and theContent-Typeheader. - Read the column. Column 1 means the string doesn't start like JSON; a larger column means valid JSON up to that point.
- Starts with
<? It's an HTML page — check the URL, the auth redirect, and the server's error handling. - Starts with a plain word? It's a text error or a stringified
undefined. Check for missing guards beforeJSON.parse. - Starts with punctuation or a function name? Look for an anti-hijacking prefix or JSONP wrapper.
- Column greater than 1 and near a bare word? Look for
NaN,Infinity,undefinedor single quotes inside an otherwise valid document.
Related: Unexpected Token in JSON covers the same failures in Chrome's wording, and Unexpected End of JSON Input covers the empty-body case Firefox reports as “unexpected end of data”.
๐งช Paste the response and see the real error
JSON Lab runs your browser's own parser, so in Firefox you will see the exact message from this article. Formatting, validation and every example above happen entirely client-side, with nothing uploaded. The one exception is AI Repair: choosing it sends your current input to Gemini to generate a suggested fix, so skip that button for anything sensitive.
Open JSON Lab โ