Quick answer
JSON.parse read one complete JSON value, then found more text after it. It accepts exactly one value per call. The three real causes are NDJSON / JSON Lines fed to JSON.parse (parse line by line), a doubled or double-encoded body, and a stray trailing character. Inspect the raw string with response.text() first.
The exact error string
JSON.parse('{"a":1}{"b":2}');
// V8 / Chrome / Node:
// SyntaxError: Unexpected non-whitespace character after JSON data
// at JSON.parse (<anonymous>)
// Firefox:
// SyntaxError: unexpected non-whitespace character after JSON data
// Safari:
// SyntaxError: Unexpected token '{'
The wording differs by engine, but the meaning is identical: a valid JSON value was parsed, and then something non-whitespace appeared after it. JSON.parse parses a single value — one object, one array, one string, one number — and nothing more.
Why JSON.parse rejects a second value
The JSON grammar defines a document as exactly one value, optionally surrounded by whitespace. JSON.parse reads that one value greedily, then verifies the rest of the string is only whitespace. The instant it meets a non-whitespace character — the { of a second object, a comma, a stray byte — it throws.
This is the mirror of Python's json.decoder.JSONDecodeError: Extra data: same root cause (more than one value), different runtime. If you also work in Python, that page covers the identical problem with json.loads.
Cause 1: NDJSON / JSON Lines (the most common)
NDJSON (newline-delimited JSON, also called JSON Lines or .jsonl) stores one JSON object per line. Log pipelines, LLM streaming responses, BigQuery exports, and many APIs use it. The whole file is many values, so a single JSON.parse fails on the second line:
const ndjson = `{"id":1,"name":"Ada"}
{"id":2,"name":"Bob"}
{"id":3,"name":"Cay"}`;
JSON.parse(ndjson); // ❌ Unexpected non-whitespace character after JSON data
// ✅ Parse each line on its own:
const rows = ndjson
.split('\n')
.filter(line => line.trim() !== '')
.map(line => JSON.parse(line));
// rows = [{id:1,...}, {id:2,...}, {id:3,...}]
If the data arrives as a stream (fetch with a chunked body, an LLM token stream), buffer the text and split on newlines, parsing only complete lines — the last chunk may be a partial line you should hold until the next chunk arrives.
Parsing an NDJSON stream from fetch
When NDJSON arrives over the network — a log tail, an LLM token stream, a large export — chunks do not align to line boundaries. A naive split on each chunk will eventually try to parse a partial line and fail. Buffer the text, emit only complete lines, and parse the remainder at the end:
const res = await fetch('/api/events'); // NDJSON stream
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let nl;
while ((nl = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, nl).trim();
buffer = buffer.slice(nl + 1); // keep the partial remainder
if (line) handle(JSON.parse(line)); // one complete value
}
}
if (buffer.trim()) handle(JSON.parse(buffer.trim())); // last line, no trailing \n
The key detail is buffer = buffer.slice(nl + 1): whatever follows the last newline is an incomplete line and must wait for the next chunk. Parsing the whole buffer per chunk is exactly what produces Unexpected non-whitespace character after JSON data on streamed data.
Cause 2: a doubled or double-encoded body
Two complete values concatenated usually means the producer emitted the body twice, or a value was JSON.stringify-ed and then wrapped and stringified again. The fix is at the source, but you confirm it by inspecting the raw text:
const res = await fetch('/api/data');
const raw = await res.text(); // read raw FIRST, don't call .json() yet
console.log(JSON.stringify(raw)); // e.g. "{\"ok\":true}{\"ok\":true}"
// If you see two objects back to back, the server is writing the body twice
// (e.g. res.json(x) called twice, or res.write + res.json). Fix the handler.
// Only call .json() once you know the body is a single value:
const data = JSON.parse(raw);
Reading the body with .text() first is the key debugging move — but remember a response body can only be read once, so read it as text and then JSON.parse the text, rather than calling .json() afterwards. (See body stream already read.)
Cause 3: a stray trailing character
Sometimes only one or two extra characters follow a perfectly good value: a trailing comma, a duplicated closing brace, a stray semicolon copied from code, an extra quote, or a byte-order mark (BOM) at the end of a concatenation. The reported position points right at it:
JSON.parse('{"ok":true},'); // ❌ trailing comma after the object
JSON.parse('{"ok":true}}'); // ❌ extra closing brace
JSON.parse('{"ok":true};'); // ❌ stray semicolon (copied from JS source)
// Fix at the source. If you must defend against trailing junk you control,
// trim known characters — never blindly slice, which can corrupt valid data:
const cleaned = raw.trim().replace(/;+$/, '');
JSON.parse(cleaned);
Debugging checklist
- ✓ Log the raw string (and its
.length) before parsing — never parse blind - ✓ One JSON object per line? It is NDJSON — split on
\nand parse each line - ✓ Two values back to back? The source is producing a doubled body — fix the producer
- ✓ Only a character or two extra? Remove the trailing comma / brace / semicolon / BOM
- ✓ Using
fetch? Readresponse.text()once, thenJSON.parseit - ✓ Getting the opposite (empty/cut-off)? That is Unexpected end of JSON input, not this
Frequently Asked Questions
What does 'Unexpected non-whitespace character after JSON data' mean?
It means JSON.parse successfully read one complete JSON value, then found more non-whitespace text after it. JSON.parse accepts exactly one value per call, so anything beyond the first value — a second object, a stray character, or another line — triggers the SyntaxError.
Why does this happen with NDJSON or JSON Lines?
NDJSON (newline-delimited JSON / JSON Lines) puts one JSON object per line, so the whole file is many values, not one. JSON.parse reads the first line then errors on the second. Split the text on newlines and JSON.parse each non-empty line individually instead of parsing the whole blob.
How is this different from 'Unexpected end of JSON input'?
They are opposites. Unexpected end of JSON input means there was too little data — the value was cut off before it finished (often an empty body). 'Unexpected non-whitespace character after JSON data' means there was too much — a complete value was found, then extra content followed it.
What causes a doubled or double-encoded JSON body?
A doubled body happens when a response is accidentally written twice, or when a value was JSON.stringify-ed twice and then concatenated. Inspect the raw text with response.text() before calling .json(); if you see two objects back to back like {"a":1}{"b":2}, the source is producing two values where one is expected.
How do I find the extra character?
Log the raw string and its length, and look at the position the error reports. Common culprits are a trailing comma, a duplicated closing brace, a stray semicolon, an extra quote, or a BOM. Pasting the text into a JSON validator highlights exactly where the second value or stray character begins.
Does the error wording differ between browsers?
Yes. V8 (Chrome, Node) says 'Unexpected non-whitespace character after JSON data', Firefox says 'unexpected non-whitespace character after JSON data' (and sometimes 'unexpected character'), and Safari may say 'Unexpected token' with a position. The cause is identical — extra content after the first JSON value.
Paste the body, see where the second value starts
Drop the raw response into the validator to find exactly where the extra content begins — nothing is uploaded to a server.