Why does JSON.parse fail at position 0?
Because the file starts with a byte order mark (U+FEFF) before the opening { or [. A BOM is a single invisible code point some tools write at the start of a UTF-8 file to mark its encoding. It has no visible glyph, so the file looks completely normal in most editors — but a JSON parser reads character by character from position 0, hits the BOM first, and stops immediately because U+FEFF is not part of the JSON grammar.
This is almost always a one-character problem. The rest of the file is typically valid JSON; deleting the invisible first character fixes the parse without touching anything else. The trap is that you cannot select or delete what you cannot see, which is why this error confuses people far more than its actual cause deserves.
What the error looks like, by environment
The exact wording differs by JavaScript engine and language, but the trigger is the same invisible character in every case:
| Environment | Typical error text |
|---|---|
| V8 (Chrome, Node.js) | Unexpected token '[BOM]', "[BOM]{..." is not valid JSON |
| Older V8 versions | Unexpected token in JSON at position 0 |
| SpiderMonkey (Firefox) | JSON.parse: unexpected character at line 1 column 1 |
| Python 3 (json module) | json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig) |
Python's message is the friendliest of the four — it names the cause and the fix in the same line, which is a useful hint even if you hit this in JavaScript instead.
How do I fix it in JavaScript or Node.js?
Strip the BOM from the string before handing it to JSON.parse:
const raw = fs.readFileSync("data.json", "utf8");
const data = JSON.parse(raw.replace(/^\uFEFF/, ""));fs.readFileSync(path, "utf8") does not strip a BOM on its own — it decodes the bytes and leaves U+FEFF as the first character of the resulting string, so the replace has to happen explicitly. The same fix applies to a string from fetch(url).then(r => r.text()) or a pasted API response: strip /^\uFEFF/ before parsing, not after.
How do I fix it in Python?
Open the file with the utf-8-sig codec instead of utf-8. It detects a leading BOM during decoding and discards it, so json.load never sees the invisible character:
import json
with open("data.json", encoding="utf-8-sig") as f:
data = json.load(f)For a string already in memory — for example the body of an HTTP response — re-decode it the same way: text.encode().decode("utf-8-sig") before passing it to json.loads. A long-standing CPython tracker issue asked for json.load to strip a BOM automatically; it was closed without that change, so utf-8-sig remains the documented workaround rather than a temporary one.
Where does the BOM come from in the first place?
Almost always from Windows-side tooling that writes UTF-8 with a BOM by default or by habit:
- Windows Notepad added a BOM to every UTF-8 file it saved for years; Notepad in the Windows 10 May 2019 Update switched the default to UTF-8 without a BOM, but files saved by older Notepad versions, or by other editors that copied its old behavior, still carry one.
- Excel's “CSV UTF-8” export writes a BOM on purpose, so that reopening the file in Excel displays accented characters correctly. A JSON file built from that export, or from a script that assumes the export is plain UTF-8, inherits the mark.
- PowerShell's
Out-Fileand>redirection have historically defaulted to UTF-8 with a BOM on Windows, so a JSON file generated by a PowerShell script or CI step can pick one up without anyone writing it explicitly. - Concatenation — joining a BOM-prefixed file with others, or prepending a header to an existing JSON file with a text tool that adds its own encoding marker.
Does the JSON spec allow a BOM?
It permits parsers to tolerate one, but does not require them to. RFC 8259, the JSON specification, is explicit on both sides of this: section 8.1 says implementations "MUST NOT add a byte order mark to the beginning of a networked-transmitted JSON text," while parsers "MAY ignore the presence of a byte order mark rather than treating it as an error, for the sake of interoperability." A MAY is optional by definition — it is exactly why V8, SpiderMonkey, and Python's json module all choose to reject a leading BOM instead of silently skipping it, even though the spec would allow either choice.
Finding a BOM you can't see
A byte order mark renders as nothing in a plain text view, which is exactly why it survives unnoticed until a parser rejects it. Paste the file's contents into the cleaner at the top of this page: it lists the byte order mark by name and code point in the "What changed" report, alongside any other invisible characters riding along — without uploading the text anywhere. The full invisible Unicode characters list covers U+FEFF and its neighbors in more depth, and the guide to sanitizing text for code, JSON, and CSV walks through the wider workflow of cleaning structured data safely before it reaches a parser. If the odd character showed up after a copy-paste rather than a file export, see why pasted text develops weird spacing for the more general version of this problem.