JSON.parse() is one of those functions that looks trivial in a code review. Pass it a string, get back an object. But when it fails — and it will — the error messages are famously unhelpful. "Unexpected token" at line 1, column 1 could mean anything from a stray byte to a completely empty response.
I've spent more hours than I'd like tracking down JSON parsing bugs. Most of the time, the root cause isn't where you're looking. This post covers the patterns I see over and over, with exact error messages and the fixes that actually work.
The Trailing Comma Trap
JavaScript objects allow trailing commas. JSON does not. This is the single most common parsing error I encounter, especially when people hand-write JSON or copy-paste from browser devtools.
The error message is always: SyntaxError: Unexpected token } in JSON at position n. If you see that, look for a comma right before a closing brace or bracket.
// This throws: SyntaxError: Unexpected token }
const badJSON = '{"name": "Alice",}';
JSON.parse(badJSON);
// Fix: remove the trailing comma
const goodJSON = '{"name": "Alice"}';
JSON.parse(goodJSON);If you're constructing JSON strings manually, use JSON.stringify() to avoid trailing commas. If you must write by hand, run the string through a linter like JSONLint before parsing.
The BOM That Sinks Ships
The Byte Order Mark (BOM) is a Unicode character (U+FEFF) sometimes placed at the start of text files. It's invisible in most editors, but it's a real character. When it appears before your JSON, JSON.parse() sees it as unexpected content.
I once spent half a day on a bug where a JSON API response would fail intermittently. The BOM was only present on responses from a specific load balancer that added it during a TLS termination step. The error was always "Unexpected token  in JSON".
const response = await fetch('/api/data');
const text = await response.text();
// Remove BOM if present
const cleaned = text.replace(/^\uFEFF/, '');
const data = JSON.parse(cleaned);Truncated Responses
When a network connection drops mid-stream, you get half a JSON document. The error is usually "Unexpected end of JSON input" or "Unexpected token at end of data". This is common in streaming APIs, serverless functions with cold starts, or proxies that buffer responses.
The fix is not in the parser — it's in the transport layer. Validate response completeness before parsing. Check Content-Length headers or use streaming with proper termination.
const response = await fetch(url);
const expectedLength = response.headers.get('Content-Length');
const reader = response.body.getReader();
let received = 0;
let chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
}
const fullBody = new TextDecoder().decode(concatUint8Arrays(chunks));
if (expectedLength && received !== Number(expectedLength)) {
throw new Error(`Truncated response: expected ${expectedLength} bytes, got ${received}`);
}
const data = JSON.parse(fullBody);Quotes and Escapes: The Double Trouble
JSON strings must be double-quoted, and any double quote inside a string must be escaped as \". Newlines must be \n. Tabs as \t. If you're building JSON from user input or unescaped templates, you'll get malformed JSON.
The error message might be "Unexpected token <" if an unescaped quote ends the string early, or "Expected ',' or '}'" if a newline breaks the parser.
const userInput = 'He said "Hello"';
// Bad: unescaped quotes
const badJSON = `{"message": "${userInput}"}`;
// JSON.parse(badJSON) throws
// Good: escape the input
const safeInput = userInput.replace(/"/g, '\\"');
const goodJSON = `{"message": "${safeInput}"}`;
// Or better: use JSON.stringify
const bestJSON = JSON.stringify({ message: userInput });The Case of the Missing Content-Type
I once worked with an API that returned JSON but with Content-Type: text/plain. The fetch response.json() method expects application/json by default. When it tried to parse the body as JSON, it actually parsed the text correctly — but if the text had any extra whitespace or formatting, it would fail.
The fix: always handle text responses manually and pass to JSON.parse() yourself, or set the correct Content-Type header. But more importantly, log the raw response body when parsing fails.
The Phantom Parsing Failure
- 14:32Deploy new version of payment service.
- 14:35Alerts: 50% of checkout requests failing with 'Unexpected token' errors.
- 14:40Check logs: error message says 'Unexpected token <' — suggests HTML, not JSON.
- 14:42Discover that the API gateway returns an HTML error page for 503 status, but the client code assumes all 2xx responses are JSON.
- 14:45Fix: check response.ok before parsing JSON, and handle non-JSON responses gracefully.
Lesson
Never assume a successful HTTP status means the body is valid JSON. Always validate the response content type and check for errors before parsing.
Tooling to the Rescue
When the JSON string is large or deeply nested, manual inspection is maddening. Here are the tools I use:
- jq: command-line JSON processor that gives precise error locations. Try jq . broken.json and it will tell you the exact line and character.
- JSONLint: online validator that highlights the error position.
- VS Code: built-in JSON validation with squiggly underlines.
- Node.js: run JSON.parse() in a try/catch and log the error message and the substring around the error position.
# Using jq to find the error
$ echo '{"name": "Alice",}' | jq .
parse error: Expected value before ',' at line 1, column 21Defensive Parsing: A Pattern That Works
Don't just wrap JSON.parse() in a try/catch and hope. Log the cause. Log a snippet of the string. Distinguish between a network error and a parsing error. Here's a utility function I use in production:
function safeParseJSON(text, context = '') {
if (typeof text !== 'string') {
throw new TypeError(`Expected string, got ${typeof text}`);
}
try {
return JSON.parse(text);
} catch (err) {
const snippet = text.substring(0, 200);
const errorInfo = {
message: err.message,
context,
snippet,
length: text.length,
firstChar: text.charCodeAt(0),
lastChar: text.charCodeAt(text.length - 1),
};
console.error('JSON parse error:', errorInfo);
throw err;
}
}The most expensive debugging sessions are the ones where you don't log the raw input. Log the string, even if it's huge. Truncate it, but log it.
Summary: Parse Defensively, Log Aggressively
JSON parsing errors are almost never about the parser itself. They're about the data. Trailing commas, BOM, truncated responses, wrong content types, unescaped quotes — these are the real culprits. Catching them early means logging the raw string, checking the response integrity, and never assuming the data is well-formed.
Next time JSON.parse() throws, don't stare at the error message. Look at the actual bytes. The answer is there.
Frequently asked questions
Why does JSON.parse() throw 'Unexpected token' error?
This error usually means the JSON string contains an unexpected character at a specific position. Common causes include trailing commas, unescaped quotes, missing quotes around keys, or invisible characters like BOM. The error message includes the line and column number to help locate the issue.
Can JSON.parse() handle single quotes instead of double quotes?
No. JSON requires double quotes for strings and property names. Single quotes are not valid JSON. If you have single-quoted strings, you'll need to replace them before parsing, but be careful not to break apostrophes inside the strings.
How do I debug a JSON.parse() error when the string is very large?
Extract the part of the string around the error position. The error message gives you a line and column. Use substring() or slice() to grab a few hundred characters around that point and log it. Tools like jq can also parse large JSON files and give precise error locations.
What is the difference between JSON.parse() and eval() for parsing JSON?
JSON.parse() only accepts valid JSON and is safe. eval() can execute arbitrary JavaScript code in the string, making it a security risk. Always use JSON.parse() for parsing JSON data.