Bad Control Character in JSON String: Every Cause and Fix

Published 14 Sep 2026 · 7 min read

This one is easy to misdiagnose because it looks like "Unexpected token in JSON"'s quieter cousin. The parser isn't confused about structure โ€” it knows it's reading a string โ€” but it hits a raw byte the JSON grammar forbids inside one: SyntaxError: Bad control character in string literal in JSON at position N. Every case below comes from the same root cause: a real, unescaped control character (a line break, tab, carriage return, or similar) sitting inside a string value instead of its escaped form. Five ways that actually happens, each with a live, prefilled JSON Lab example so you can watch the exact string fail instead of reading a description of one.

Quick answer: JSON's string grammar requires every ASCII control character (0x00–0x1F) inside a string to be escaped — a real line break has to be written as \n, a real tab as \t, and so on. If the raw byte is sitting in the string instead of its two-character escape sequence, JSON.parse refuses it the instant it reads that byte. The fix is almost always upstream: whatever built the JSON string concatenated or interpolated raw text instead of running it through JSON.stringify(), which escapes control characters automatically.

Quick reference

All five results below were verified live against Node's V8 engine before publishing โ€” the same engine every Chromium-based browser uses. Firefox (SpiderMonkey) and Safari (JavaScriptCore) implement JSON.parse independently, so exact wording and position numbers can differ slightly even though the same byte fails everywhere.

SymptomMost likely causeFix
Error position lands mid-string, right after pasted multi-line textRaw newline from a copy-pasted multi-line valueEscape as \n, or rebuild the JSON with JSON.stringify()
Error lands between two words that should be tab-separatedRaw tab from pasted spreadsheet/TSV dataEscape as \t, or strip tabs before embedding
Error appears in JSON built from a JS template literalA multi-line template literal's real line breaks leaked into a string valueNever hand-format JSON in a template literal — build an object and JSON.stringify() it
Error only shows up in JSON built from captured terminal/log outputRaw carriage return (\r) from terminal or Windows-style log linesEscape as \r, or normalize line endings before embedding
Error shows up in text copied from a PDF or rich documentStray control bytes (vertical tab, form feed) left behind by extractionSanitize extracted text before embedding, or use JSON.stringify() from the start

1. A raw newline from pasted multi-line text

The most common version: a comment, address, or free-text field is copied from somewhere that preserves real line breaks, then dropped straight into a hand-built JSON string without escaping. The JSON looks fine to the eye โ€” it's still multi-line in whatever editor is showing it โ€” but the parser sees an unescaped control byte the moment it crosses the line boundary inside the string.

// Bug: the real newline between "one" and "Line two" is embedded literally
const json = '{"comment": "Line one\nLine two"}'; // \n here is a JS string escape,
                                                    // producing an ACTUAL newline byte,
                                                    // not the two characters \ and n
JSON.parse(json);
// SyntaxError: Bad control character in string literal in JSON at position 21

The fix is to never build this string by hand at all: pass the real object through JSON.stringify(), which turns the actual newline into the two-character escape sequence \n automatically.

2. A raw tab from pasted spreadsheet data

Copying a row out of a spreadsheet or a TSV export and dropping it into a JSON string value carries the real tab characters between columns along with it. Same failure mode as the newline case, different byte: 0x09 instead of 0x0A.

// Bug: real tabs from a copied spreadsheet row, not escaped
const json = '{"row": "Alice\tSmith\t42"}';
JSON.parse(json);
// SyntaxError: Bad control character in string literal in JSON at position 14

If the goal is actually tabular data, this is usually a sign the value shouldn't be one opaque string at all โ€” splitting it into an array or object of real fields avoids the escaping question entirely. If a literal tab has to stay inside a single string, escape it as \t.

3. A multi-line template literal used to hand-format JSON

JavaScript template literals make it tempting to "pretty-print" JSON by hand, spreading a query or payload across multiple source lines for readability. Every line break inside the backticks is a real newline byte in the resulting string โ€” including the ones inside what's supposed to be a JSON string value.

// Bug: the template literal's own line break becomes part of the string value
const json = `{"query": "SELECT *
FROM users"}`;
JSON.parse(json);
// SyntaxError: Bad control character in string literal in JSON at position 19

Multi-line template literals are fine for JSON *structure* readability when every string value itself stays single-line โ€” the bug is specifically a line break landing inside an open string. Building the object as real JS data and calling JSON.stringify(data, null, 2) gets the same readable, indented output without ever risking this.

4. A raw carriage return from terminal or Windows-style log output

Captured terminal output, progress-bar redraws, or log files with Windows-style \r\n line endings all carry literal carriage-return bytes. Wrapping a captured log chunk in quotes to embed it as a JSON string field โ€” common in CI tooling and error-reporting payloads โ€” carries those raw \r bytes straight into the string.

// Bug: a captured log line's raw \r embedded directly
const json = '{"log": "Build started...\rBuild finished"}';
JSON.parse(json);
// SyntaxError: Bad control character in string literal in JSON at position 25

Same fix as every case above โ€” run the captured text through JSON.stringify() rather than interpolating it into a hand-built string โ€” plus, if the source is genuinely Windows-style text, normalizing \r\n to \n before embedding avoids carrying redundant carriage returns at all.

5. Stray control bytes left behind by PDF or document text extraction

Text pulled out of a PDF, Word document, or rich-text field can carry control bytes that never render visibly โ€” vertical tabs (0x0B), form feeds (0x0C), or other leftovers from the source format's own internal layout. Because they're invisible in most editors, this is the hardest variant to spot by eye: the JSON looks completely clean until JSON.parse hits the byte a viewer never rendered.

// Bug: a vertical tab (0x0B) survived a PDF-to-text extraction, invisible in most editors
const json = '{"text": "Section 1Section 2"}';
JSON.parse(json);
// SyntaxError: Bad control character in string literal in JSON at position 19

Because these bytes are invisible, the reliable fix is sanitizing extracted text at the source โ€” strip control characters outside the small set JSON already escapes correctly (\n, \r, \t) before the value is ever assembled into JSON, rather than trying to spot one by eye in an editor that won't show it.

Debugging checklist

  1. Check the error position against the string value nearest it โ€” the offending byte is inside that string, not at the structural level, so single-quote/trailing-comma/HTML-page fixes won't help here.
  2. Ask where the JSON was built. If it came from string concatenation, interpolation, or a template literal instead of JSON.stringify(), that's almost always the root cause.
  3. If the source text came from a paste, a captured log, or an extracted document, assume it may carry invisible control bytes even if it looks clean in your editor.
  4. Fix it upstream โ€” regenerate the JSON via JSON.stringify() โ€” rather than manually deleting whatever character the error position points at, which just moves the problem to the next stray byte.

Two related JSON failures worth knowing the difference from: unexpected token in JSON means the parser doesn't recognize the structure it's looking at, and unexpected end of JSON input means the input ran out before a value finished. A bad-control-character error is narrower than both โ€” the parser is inside a valid string, mid-value, and rejects one specific unescaped byte.

๐Ÿงช Paste your broken JSON and see the real error

JSON Lab runs the same parser your browser does — formatting and every example above happen entirely client-side, with nothing uploaded. Two features are the exception: AI Repair sends your current input to Gemini to generate a suggested fix, and Schema Validation sends both the JSON data and the schema you're validating against. Skip both buttons for anything sensitive and stick to manual fixes from this guide instead.

Open JSON Lab โ†’

โ† Back to Blog · See also: Unexpected Token in JSON โ†’