What the error looks like
You'll see one of these in different parsers:
SyntaxError: Bad control character in string literal in JSON at position 42
json.decoder.JSONDecodeError: Invalid control character at: line 3 column 12 (char 42)
JsonReaderException: Bad control character in string literal Every form points at the same problem: the parser walked into a string and hit a byte it isn't allowed to consume raw.
The four most common causes
1. A literal newline in a "multi-line" string
JSON has no syntax for multi-line strings. This is invalid:
{
"message": "line one
line two"
}
Fix: join the lines with the escape \n:
{"message": "line one\nline two"}
If you're hand-editing, run the value through
JSON.stringify first. If you need real multi-line
readability, switch to YAML or store
the value as an array of lines.
2. A literal tab from copy-paste
Tabs (U+0009) are invisible. Pasting from a spreadsheet, log file, or CSV often drops them right into your JSON.
{"label": "name value"} ← tab between "name" and "value"
Fix: replace tabs with \t, or strip them entirely.
Most editors offer "Show invisible characters" to spot them.
3. A Windows-style line ending mid-string
A \r\n pair pasted from a Windows source becomes a
raw \r + raw \n inside the string —
both forbidden.
Fix: normalize line endings before parsing, or escape both as
\r\n.
4. String concatenation instead of serialization
The classic backend bug: building JSON with string templates.
// JS — broken
const body = `{"note": "$${userInput}"}`;
If userInput contains a newline, the result is
invalid JSON. Use the language's serializer:
const body = JSON.stringify({ note: userInput }); How to fix programmatically
If you have a string of "almost JSON" and need to repair the embedded control characters, escape them before parsing:
// JavaScript
const safe = raw.replace(/[-]/g, (c) =>
"\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")
);
const data = JSON.parse(safe);
# Python
import re, json
safe = re.sub(r"[\x00-\x1f]", lambda m: f"\\u{ord(m.group()):04x}", raw)
data = json.loads(safe) This is a workaround, not a fix — the real fix is in the producer. If you don't control it, log a bug upstream while you patch downstream.
Quick reference: which characters break which parsers
| Character | Code point | Allowed raw? | Escape |
|---|---|---|---|
| NUL | U+0000 | No | |
| Tab | U+0009 | No | \t |
| Newline (LF) | U+000A | No | \n |
| Carriage return | U+000D | No | \r |
| Form feed | U+000C | No | \f |
| Backspace | U+0008 | No | \b |
| DEL | U+007F | Yes (technically) | — |
Defined in RFC 8259 §7.
Related JSON errors
- Unexpected token errors — trailing commas, single quotes, BOMs.
- Comments in JSON — why
//and/* */fail.
FAQ
What counts as a control character?
Any code point in U+0000–U+001F: NUL, tab (U+0009), newline (U+000A), carriage return (U+000D), and the rest of C0. JSON strings forbid them as raw bytes — they must appear as escape sequences (\t, \n, \r, or \u00XX).
Why does my multi-line string fail?
JSON has no multi-line string literal. A literal newline inside a string is a control character and is invalid. Either join the lines with \n inside a single-line string, or use a format that supports multi-line (YAML, TOML).
I copied this from a database / log file. Why?
Logs and database dumps frequently contain raw tabs and newlines. Wrapping that text in quotes does not escape it. You need to JSON.stringify the string in your producer, not just concatenate it into a JSON template.
Why does Python's json.dumps escape these for me but my hand-written JSON doesn't?
Because json.dumps walks the string and emits the escape sequence for each control character. If you build JSON by string concatenation ("{\"x\":\"" + value + "\"}"), you bypass that and rely on the value already being JSON-safe — which strings rarely are.
What's the difference between this and 'Unexpected token' errors?
Unexpected-token errors fire when the parser sees a structural character it didn't expect (a comma, a brace). Bad-control-character fires specifically when a forbidden byte appears inside a string. Different cause, different fix.