Unexpected Non-Whitespace Character After JSON: Every Cause and Fix
This one trips people up in a specific way: JSON.parse() doesn't fail. It succeeds — it finds a complete, valid value, right at the start of the string — and only then throws, because something else is sitting after it. The message is SyntaxError: Unexpected non-whitespace character after JSON at position N in V8 (Chrome, Node) or JSON.parse: unexpected non-whitespace character after JSON data in Firefox. Five real causes below, each verified with a live, prefilled example in JSON Lab so you can watch the exact string fail instead of reading a description of one.
JSON.parse() reads exactly one JSON value and stops. Trailing whitespace after that value is fine and gets ignored; anything else — another value, a stray character, a whole extra line — throws this exact error, and it throws after the parse of the real value already succeeded. The position number in the message is the offset of the first non-whitespace character left over after that value — exactly where the value ends if nothing separates them, or a few characters later if whitespace comes in between — which is usually all you need to find the leftover text and identify which cause below applies.
Quick reference
All five results below were verified live in JSON Lab and cross-checked against Node's V8 engine before publishing — every one throws the identical Unexpected non-whitespace character after JSON message, not a different error that merely sounds similar.
| Symptom | Most likely cause | Fix |
|---|---|---|
Position lands right where a second { or [ begins | Two or more JSON values concatenated (NDJSON) parsed as one | Split on newlines and parse each line, or use a streaming/NDJSON parser |
Position lands one character past the final } | A trailing ; copied along with a JS statement | Trim to the object literal only — drop the semicolon, and any leading const x = |
| Position lands one character past what looks like a complete object | A stray extra } or ] left over from a manual edit | Diff against a known-good copy; delete the extra bracket, don't add one to "balance" it |
| Position lands at the start of a second line of plain text | Debug/log output appended after the real JSON response body | Find and remove the stray res.write() call or response-writing middleware after the endpoint's JSON write (in Node — plain console.log() never reaches the client), or the leftover echo/print in a PHP/CGI script |
Position lands at a line starting with data: | A streaming/SSE sentinel left attached to a manually-buffered response | Strip the sentinel line (e.g. data: [DONE]) before parsing the accumulated buffer |
1. Two or more JSON values concatenated (NDJSON parsed as one)
Newline-delimited JSON is a real, common wire format — a logging endpoint or an event stream that writes one JSON object per line rather than wrapping everything in a single array. It's not a bug in the data. The bug is calling plain JSON.parse() on the whole multi-line buffer at once, when the format was never meant to be parsed as a single value in the first place.
const buffer = '{"event":"user.login","userId":42}\n' +
'{"event":"user.logout","userId":42}';
JSON.parse(buffer);
// SyntaxError: Unexpected non-whitespace character after JSON at position 35
// The first line parsed fine on its own — the second line is what's "extra"
The fix is to stop treating NDJSON as one JSON document: split on newlines (guarding against a trailing blank line) and parse each line separately, or use a proper NDJSON/streaming parser if the source is large enough that holding the whole buffer in memory isn't practical.
const events = buffer
.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
See it fail live
Two valid, complete JSON objects, newline-separated. Paste it in JSON Lab and the parser stops exactly where the second object begins:
Open this example in JSON Lab2. A trailing semicolon copied along with a JS object literal
Copying JSON out of a script file is easy to get slightly wrong: grab from the opening { to what looks like the matching closing }, and if the source line ends in }; — because it was a JS statement, not a bare JSON document — the semicolon comes along for the ride. It reads as harmless. JSON.parse() disagrees.
// Source file: const config = {"apiUrl":"https://api.example.com","timeout":5000};
// Copied everything from the { to the ; :
const pasted = '{"apiUrl":"https://api.example.com","timeout":5000};';
JSON.parse(pasted);
// SyntaxError: Unexpected non-whitespace character after JSON at position 51
The fix is purely mechanical: strip anything that isn't part of the JSON value itself — no leading const x =, no trailing ;. If this happens often enough to be worth automating, a quick str.replace(/;\s*$/, '') before parsing is a reasonable guard, but fixing the copy is more reliable than patching around a bad one.
See it fail live
A real object literal with the statement-terminating semicolon still attached:
Open this example in JSON Lab3. A stray extra closing brace left from a manual edit
This one is almost always human, not automated: merging two config blocks by hand, deleting a nested object but leaving one of its closing braces behind, or duplicating a snippet and only editing the inside. The result still looks complete at a glance — brace-counting by eye is unreliable past a certain size — but there's one bracket too many at the end.
const edited = '{"retries":3,"timeout":30}}'; // one extra } left over
JSON.parse(edited);
// SyntaxError: Unexpected non-whitespace character after JSON at position 26
// Everything up to position 26 parsed as a complete, valid object
The reliable fix isn't counting braces manually — it's diffing against a known-good version of the same file, or running the suspect text through a formatter that shows tree structure (so an unmatched bracket becomes visually obvious) before assuming the fix is to add a matching bracket somewhere else instead of deleting the extra one.
See it fail live
A small, otherwise-valid object with one leftover } at the very end:
4. Debug or log output appended after the real JSON response body
This is a server-side bug that shows up client-side: an API endpoint is supposed to return nothing but JSON, but somewhere after the json_encode()/JSON.stringify() call — or, worse, after the framework has already sent the response — a stray write lands on the same stream the client is reading. In PHP or a plain CGI script, that's as simple as a leftover echo or print after json_encode() — both write straight to the response body. In Node, it takes a more specific mistake: a debug res.write() meant for a logger, or a middleware that appends its own text after the real handler already wrote the JSON — plain console.log() alone would never reach the client at all, since it only writes to the server's own stdout. Either way, the client receives one HTTP body containing both.
// Server (illustrative): a stray res.write() meant for a logger landed on
// the response stream instead of the server's own log output.
res.write(JSON.stringify({ status: 'ok', id: 987 }));
res.write('\nWarning: cache miss for key user:987'); // meant for the logs,
// accidentally sent to the client
// Client:
const body = '{"status":"ok","id":987}\nWarning: cache miss for key user:987';
JSON.parse(body);
// SyntaxError: Unexpected non-whitespace character after JSON at position 25
Client-side there's nothing to "fix" in the parsing — the payload is genuinely malformed at the source. The real fix is on the server: find whatever still writes to the response stream after the JSON body (a misplaced debug statement, a middleware that appends a footer, an error handler that both sends JSON and logs to the same connection) and remove it, or route it to the actual server logs instead.
See it fail live
A valid JSON response with a stray log line appended on the next line, exactly as a client would receive it over the wire:
Open this example in JSON Lab5. A streaming sentinel left attached to a manually-buffered response
Server-Sent Events and streaming chat APIs (this exact failure shows up regularly against OpenAI-style streaming endpoints) send a series of data: ... lines and terminate the stream with a sentinel line like data: [DONE]. Code that reads the raw stream and hand-rolls its own buffering — instead of using an SSE client that already understands the framing — can end up concatenating every chunk, sentinel included, and then calling JSON.parse() on the whole thing as if it were one JSON document.
// Chunks accumulated manually from a ReadableStream, sentinel included
const accumulated = '{"choices":[{"delta":{"content":"done"}}]}\n' + 'data: [DONE]';
JSON.parse(accumulated);
// SyntaxError: Unexpected non-whitespace character after JSON at position 43
The fix is to parse the stream the way its own protocol expects: each data: ... line is its own event, parsed independently, and the literal string [DONE] is a sentinel to check for and stop on — not a JSON value to parse. Most SSE and streaming-API client libraries already do exactly this; reaching for one instead of buffering raw chunks by hand removes this failure mode entirely.
See it fail live
A valid streaming-response JSON chunk with the stream's own [DONE] sentinel still attached:
Debugging checklist
- Read the position number in the error and count to that exact character offset in the raw string. That's where the leftover data actually starts — the same point where the valid value ends only if no whitespace separates them — not where anything is "broken" in the usual sense.
- Check what comes right after that offset. If it's another
{or[, suspect concatenated values (NDJSON) before anything else. - If what follows is a single stray character (
;, an extra}/]), suspect a copy-paste or manual-edit artifact rather than a server bug. - If what follows is a full line of readable text or a
data:-prefixed line, suspect appended debug output or a streaming sentinel rather than anything wrong with the JSON itself. - If the source is a server response you control, reproduce it with a raw request (curl, or your browser's network tab) rather than through whatever client code is failing — confirms whether the extra bytes are really on the wire or introduced by the client-side buffering.
Two related JSON.parse failures worth knowing apart from this one: Unexpected token in JSON fires when a character can't even start a valid value, often at position 0 — the parse never gets going. Unexpected end of JSON input fires when the string runs out before a value finishes — it stops too early. This error is the mirror image of both: the JSON is completely valid, and the only problem is what comes after it.
📋 Paste it and see exactly where it stops
JSON Lab formats, validates and tree-views JSON entirely in your browser — nothing you paste is uploaded anywhere, and the error position lines up with the exact character in your input.
Open JSON Lab →