I was sitting at my desk at 2 AM, staring at a Node.js service that kept crashing. The error log was a broken record:
SyntaxError: Unexpected token in JSON at position 0
The JSON came from a partner API that had worked perfectly for months. Nothing changed on our end. My first instinct was to suspect the API changed their response format. But when I curled the endpoint and saved the response, JSON.parse() still blew up the same way.
The worst part? Position 0 errors are notoriously misleading. They rarely mean there's actually nothing at position 0 — they mean the parser found something it can't identify.
What "Position 0" Actually Means
JavaScript's JSON.parse() starts reading your string from index 0. If the very first character isn't one of these valid starters — {, [, ", a digit, true, false, or null — you get the "position 0" error.
But here's the trick: the error message lies to you. The "unexpected token" might literally be invisible. Let me walk through the real culprits.
1. The Byte Order Mark (BOM)
This one got me. Some APIs, especially older Microsoft-based services, prepend a UTF-8 BOM (\uFEFF or 0xEF BB BF) to their responses. Your eyes see {, but JSON.parse() sees \uFEFF{.
// This will fail at position 0
const jsonWithBOM = '\uFEFF{"name": "pipeline"}';
JSON.parse(jsonWithBOM);
// SyntaxError: Unexpected token in JSON at position 0
// Fix it
const cleaned = jsonWithBOM.replace(/^\uFEFF/, '');
JSON.parse(cleaned); // Works
How to check for BOM with online tools: Paste the raw response into JSON Formatter and hit "Validate". The tool will flag invisible characters. If you see a red error at position 0 with no visible issue, BOM is your prime suspect.
2. Empty or Whitespace-Only Strings
Sometimes the API returns nothing. Or worse — it returns spaces.
// This fails
JSON.parse('');
JSON.parse(' ');
// Always guard your parse calls
function safeParse(str) {
if (!str || str.trim().length === 0) {
throw new Error('Empty response — not JSON');
}
return JSON.parse(str);
}
I've seen this happen when a load balancer returns a 200 with an empty body during a health check interval. Your code assumes a real payload, but gets nothing.
3. Smart Quotes and Curly Apostrophes
Copy JSON from a Slack message, Notion doc, or email, and you'll get "smart quotes" — Unicode characters that look like quotes to humans but mean nothing to the parser.
// These "smart quotes" will fail
const sneaky = '{\u201Cname\u201D: \u201CAlex\u201D}';
JSON.parse(sneaky);
// SyntaxError: Unexpected token in JSON at position 0
// Replace them
const fixed = sneaky
.replace(/[\u201C\u201D]/g, '"')
.replace(/[\u2018\u2019]/g, "'");
JSON.parse(fixed); // Works
4. Hidden Control Characters
Zero-width spaces (\u200B), non-breaking spaces (\u00A0), and other invisible Unicode characters can sneak into JSON from text editors, API gateways, or database outputs.
const polluted = '\u200B{"status": "ok"}';
JSON.parse(polluted); // Position 0 error
// Strip non-visible characters
const sanitized = polluted.replace(/[\u200B-\u200D\uFEFF\u00A0]/g, '');
JSON.parse(sanitized); // Works
The Real Debugging Workflow
When you hit position 0 errors, stop guessing. Here's my battle-tested process:
- Log the raw bytes, not the string. Use
Buffer.from()orcharCodeAt(0)to reveal hidden characters. - Paste into a validator that shows byte positions. The JSON Formatter tool's validation feature pinpoints exact positions and highlights non-printable characters.
- Check content-type headers. BOMs often correlate with
charset=utf-8responses from Windows-based servers. - Write a sanitization middleware that strips BOM and control characters before parsing.
function sanitizeJsonResponse(raw) {
return raw
.replace(/^\uFEFF/, '') // Strip BOM
.replace(/[\u200B-\u200D\uFEFF]/g, '') // Zero-width chars
.replace(/[\u201C\u201D]/g, '"') // Smart double quotes
.replace(/[\u2018\u2019]/g, "'"); // Smart single quotes
}
// Usage
const raw = await response.text();
const clean = sanitizeJsonResponse(raw);
const data = JSON.parse(clean);
Common Gotchas
One thing that tripped me up for hours: if you're using response.json() in the browser's Fetch API, the browser may or may not strip BOM depending on the implementation. Always .text() first, then sanitize, then JSON.parse().
Another trap: PostgreSQL and MySQL both store JSON with invisible characters sometimes, especially when you paste from a GUI tool. Always run a validation pass before pushing JSON into your database.
FAQ
Q: Can the BOM appear at positions other than 0?
A: Technically, a BOM can appear in the middle of a UTF-16 file, but in JSON, it's almost always at position 0. If you see unexpected characters at position 0, strip \uFEFF first.
Q: Why does JSON.parse show position 0 when the character isn't at the start?
A: The parser may advance past whitespace internally and report position 0 as a fallback. Always check the actual first visible character's byte offset.
Q: Does JSON.parse() handle BOM in modern Node.js?
A: No. Node.js and browsers both reject BOM-prefixed JSON strings. You must strip it manually.
Q: What's the fastest way to detect hidden characters?
A: Log JSON.stringify(input.substring(0, 20)) — it escapes control characters and reveals what's really there. Or paste into the JSON Formatter tree view which highlights non-printable characters.
Q: Are there legitimate cases where position 0 errors aren't hidden chars?
A: Yes. If your API returns HTML instead of JSON (like a 404 page), the parser chokes on <. Always check the HTTP status code before parsing.
Q: How do I handle this in TypeScript?
A: Define a SafeJsonResponse type wrapper around string, and pass all raw responses through a sanitizer function before type casting.
Q: Should I always strip BOM?
A: Yes. There's zero use case for BOM in modern JSON. Even Microsoft's own documentation recommends stripping it for interoperability.
Q: Does your formatter tool strip hidden characters automatically?
A: The JSON Formatter validates and shows you exactly where hidden characters appear. Paste your JSON, hit "Validate," and the error panel will tell you if BOM or zero-width chars are present, along with their hex codes.