U+2028 and U+2029 in JSON and JavaScript: what actually breaks
JSON.parse does not break. Measured on Node 22, current Chromium and Python 3.13: a raw U+2028 or U+2029 inside a JSON string parses cleanly in all three. What actually differs is the serializers — JavaScript writes these characters out raw, Python escapes them. And inside JavaScript source code, both characters still count as line terminators, which is where the real bugs come from.
Measured, one row at a time
Each row is a single expression evaluated on Node 22. U+2028 is the line separator,
U+2029 the paragraph separator; both are invisible on screen.
| Where the character sits | Result |
|---|---|
Inside a JSON string, parsed with JSON.parse | OK — parses fine |
Inside a JS string literal ("x
y") | OK |
| Inside a template literal | OK |
| Between two statements, acting as whitespace | OK |
Inside a // comment, with code after it | Ends the comment — the code after it runs |
| Inside a regex literal | SyntaxError: Invalid regular expression |
The last two rows are the ones that bite. A U+2028 inside a line comment terminates the comment, so whatever was meant to be commented out starts executing. A U+2028 inside a regex literal stops the literal from parsing at all. Neither of these looks like anything when you read the file, because the character has no width.
The serializers disagree
This is the part that catches people out across language boundaries. Same object, two languages:
// JavaScript, Node 22
JSON.stringify({a: "x
y"})
// -> {"a":"x
y"} raw character, NOT escaped
# Python 3.13
json.dumps({"a": "x
y"})
# -> {"a": "x\u2028y"} escaped
Python escapes them; JavaScript does not. So JSON produced by a JS service carries the raw character across the wire, while JSON produced by a Python service carries a six-character escape sequence. Both parse fine on the receiving end — until the payload is pasted into a JavaScript file instead of parsed, and the line-terminator behaviour above takes over.
The one-line fix when you generate JS from data
If you are writing JSON into a <script> tag or building JavaScript source from
data, escape these two characters explicitly. JSON.stringify will not do it for you:
const safe = JSON.stringify(data)
.replace(/\u2028/g, "\\u2028")
.replace(/\u2029/g, "\\u2029");
The output still parses to the same value — verified by round-tripping it back through
JSON.parse — but it now survives being pasted into source code.
Checking whether you have them
Both characters are invisible, so the only way to know is to look. The zero-width space remover shows you where invisible characters sit in a block of text, and the invisible characters list has the code points. For a payload that will not parse at all, start with the JSON cleaner.
What this page does not claim
These measurements come from Node 22, current Chromium and Python 3.13 — the runtimes available here. Older JavaScript engines treated U+2028 and U+2029 as line terminators everywhere, including inside string literals, which is where this character's reputation comes from; that was relaxed in ES2019, and I could not test pre-2019 engines on this machine, so this page makes no claim about them. Run the snippets above against the runtimes you actually ship to.
Measured 2026-09-21 on Node 22.22, Chromium (Playwright) and Python 3.13. Tools: Claude watermark remover, zero-width space remover, JSON cleaner, invisible characters list, what they cost in tokens.