← All articles

Bad Control Character in String Literal in JSON: Fixes

Raw tabs, newlines, null bytes, and ANSI escape codes inside a JSON string trigger this error. Learn why the JSON spec forbids them, how they sneak in, and how to strip or escape them.

SyntaxError: Bad control character in string literal in JSON at position N means there is a raw, unescaped control character — a tab, newline, carriage return, or any character in the range U+0000–U+001F — sitting inside a JSON string. The JSON specification forbids them. This article explains why, where they come from, and how to get rid of them.

Which string-error am I getting?

What Is a Control Character?

The first 32 Unicode code points (U+0000 through U+001F) are control characters — invisible formatting codes inherited from teletype machines. Some familiar ones:

Code pointNameJSON escape
U+0000Null
U+0008Backspace\b
U+0009Horizontal tab\t
U+000ALine feed (newline)\n
U+000CForm feed\f
U+000DCarriage return\r
U+001BEscape (ANSI sequences start here)

The JSON specification (RFC 8259 §7) is explicit: all characters in this range must be escaped when they appear inside a JSON string. A raw newline inside a quoted string is a syntax error, not a value.

Why Raw Newlines Break JSON

Consider a string value that contains a real newline character:

// What the bytes look like (the newline is literal, not \n):
{"message":"line one
line two"}

JSON.parse('{"message":"line one\nline two"}')
// SyntaxError: Bad control character in string literal in JSON at position 20

The parser sees the opening double-quote, reads characters, then hits a raw 0x0A byte. Because a JSON string must be a single uninterrupted sequence between two double-quotes on the same logical line, the bare newline terminates the string unexpectedly.

The correct representation uses the two-character escape sequence \n:

// Correct — \n is the escape, not a literal newline
{"message":"line one\nline two"}

JSON.parse('{"message":"line one\nline two"}') // ✓ works

How Control Characters Get Into JSON

1. Building JSON strings with template literals or concatenation

const note = `first line
second line`;                   // real newline from the template literal

const json = `{"note":"${note}"}`; // raw newline embedded in the string
JSON.parse(json);               // SyntaxError: Bad control character…

// ✓ Fix: always use JSON.stringify for values, never interpolate manually
const json = JSON.stringify({ note });  // escapes the newline automatically

2. Reading files and embedding content verbatim

import fs from 'fs';
const content = fs.readFileSync('notes.txt', 'utf8');
// content may contain \n, \r\n, \t, etc.

// ❌ Manual string building — embeds raw control characters
const json = '{"content":"' + content + '"}';

// ✓ JSON.stringify handles all escaping
const json = JSON.stringify({ content });

3. API responses from systems that don't escape output

Some back-end systems (legacy PHP scripts, custom serialisers, database triggers that build JSON manually) emit field values with unescaped newlines or tabs. You receive syntactically invalid JSON and response.json() throws.

Diagnose by logging the raw body length and first 200 characters. Look for visible line breaks inside what should be a string value.

4. Copy-pasting from a terminal (ANSI escape sequences)

Terminal output contains ANSI colour codes that start with the Escape character (0x1B) followed by [. If you paste terminal output into a JSON string, every colour reset (\x1B[0m) becomes a control character.

// Terminal output pasted into a JSON string:
{"log":"\u001B[32mOK\u001B[0m request processed"}
//      ^^^ raw ESC byte — should be the 6-char \u001B escape sequence

5. Null bytes from binary data

Reading part of a binary file, a database BLOB column, or a C-style string into a JSON field can embed null bytes (U+0000), which are the most common source of this error in database-to-JSON pipelines.

How to Fix It

Prevention: always use JSON.stringify

This is the definitive solution. JSON.stringify() correctly escapes every control character — you never need to escape manually.

// ✓ Safe regardless of what userInput contains
const json = JSON.stringify({ message: userInput });

Repair: strip or escape after the fact

If you're receiving broken JSON from an external source and can't fix the producer, you can sanitise the raw string before parsing. The safest approach is to escape bare control characters:

function escapeControlChars(raw) {
  return raw.replace(
    /[\u0000-\u001F]/g,
    (ch) => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')
  );
}

const fixed = escapeControlChars(rawFromApi);
const data  = JSON.parse(fixed);

If the control characters are noise (ANSI codes, null bytes) rather than meaningful data, stripping is simpler:

// Strip all control characters except \t, \n, \r (which are common in text)
const cleaned = raw.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, '');
const data = JSON.parse(cleaned);

Two More Real-World Sources

  • UTF-8 BOM at position 0. Files saved as "UTF-8 with BOM" begin with 0xEF 0xBB 0xBF. RFC 8259 forbids a BOM in JSON, and JSON.parse rejects it with a position-0 error that's easy to misread as a control character. Strip with text.replace(/^/, '') before parsing, or save the file as "UTF-8" (no BOM) in your editor.
  • VS Code: "Remove Control Characters". If you're cleaning up a single file by hand, VS Code's Selection → Remove All Occurrences of Find Match combined with regex find on [\x00-\x08\x0b\x0c\x0e-\x1f] (preserving real \t\n\r) deletes invisible control bytes in one pass. There's also a "Remove Control Characters" command in some extensions if you do this often.

Detecting the Offending Position

The error message includes a byte position. Use it to inspect what's there:

try {
  JSON.parse(raw);
} catch (e) {
  const pos = Number(e.message.match(/position (\d+)/)?.[1]);
  if (!isNaN(pos)) {
    const context = raw.slice(Math.max(0, pos - 20), pos + 20);
    console.log('Context around error:', JSON.stringify(context));
    // JSON.stringify re-escapes the control chars so you can see them
    // e.g. "...line one\nline two..." — the \n was a raw newline
  }
}

Frequently Asked Questions

What is a bad control character in JSON?

A raw, unescaped character in the range U+0000–U+001F — a tab, newline, carriage return, null byte, or ANSI escape — sitting inside a JSON string. RFC 8259 §7 requires every such character to be escaped (for example \n for a newline).

How do I fix a bad control character error?

The definitive fix is to build the JSON with JSON.stringify(), which escapes control characters automatically. If you're receiving broken JSON you can't fix at the source, escape or strip the control characters with a regex before parsing (see the snippets above).

Why does a raw newline break JSON?

A JSON string must be an uninterrupted sequence between two double-quotes. A literal newline byte (0x0A) ends the string prematurely, so the parser reports a control-character error. The valid representation is the two-character escape \n.

How do I remove ANSI color codes from JSON?

ANSI sequences start with the Escape byte (U+001B). Strip them with raw.replace(/\[[0-9;]*m/g, '') before parsing, or avoid pasting raw terminal output into JSON string values in the first place.

Repair Broken JSON in Your Browser

Paste your broken JSON into JSON Fix on fixjson.org. The lenient parser identifies control-character violations and reports the exact position. For simple cases it can repair the string automatically.