Unexpected end of JSON input: Empty Input or Incomplete JSON?
The common advice is that this means an empty response. That is one cause, not the rule. What the message actually tells you is where your input stopped: at a point where the parser was still waiting for a value to begin.
The short answer
Unexpected end of JSON input means the input ran out while the parser was still waiting for a value to begin. An empty response body is the most common way that happens — the input ran out at position 0 — but it is not the only one.
A truncated payload produces this same message whenever the cut lands somewhere a value was due: right after a :, right after a ,, or right after an opening [. Cut the same payload a few characters later and you get a different, more specific message instead. So the useful question is not “is it empty?” but where did my input stop?
Empty, or incomplete?
Both. These all produce the identical message on current V8:
(empty)
(whitespace only)
[
{"a":
[1,
{"a":{"b":
truThe first two are empty. The rest are not — they are perfectly ordinary truncations. What unites them is that each one stops at a point where the next thing had to be a value: after an opening bracket, after a colon, after a comma, or partway through a bare true, false, or null.
So the widely repeated advice that this error means your response was empty is right about the common case and wrong as a rule. If you have checked and the body is not empty, you have not ruled this error out — you have learned it was cut somewhere specific.
Why only some truncations produce it
When the parser runs out of input, it says what it was waiting for. Most of the time it can name one thing — a colon, a comma, a closing brace, a closing quote — and it puts that in the message along with a position.
But at the points above, what it wants is a value, and a value can be any of seven things: an object, an array, a string, a number, true, false, or null. There is no single token to name, so it falls back to the generic sentence.
One token apart, same document:
{"a":
-> Unexpected end of JSON input
{"a": 1
-> Expected ',' or '}' after property value in JSON at position 7 (line 1 column 8)This also explains an asymmetry that looks arbitrary until you know the rule: [ alone gives the generic message, but { alone does not. After [ a value may begin. After { only a property name or } may follow — nameable, so V8 names it.
Where the cut lands
The measured boundary. Everything above the divide ends where a value was due:
| Input | Where it stops | Message |
|---|---|---|
| (empty) | Position 0 — before anything | Unexpected end of JSON input |
| (whitespace only) | After whitespace, still before any value | Unexpected end of JSON input |
| [ | After [ — an element could start here | Unexpected end of JSON input |
| {"a": | After : — the value belongs here | Unexpected end of JSON input |
| [1, | After , — the next element belongs here | Unexpected end of JSON input |
| {"a":{"b": | After a nested : — same position, deeper | Unexpected end of JSON input |
| tru | Partway through true | Unexpected end of JSON input |
| { | After { — only a property name or } may follow | Expected property name or '}' in JSON at position 1 (line 1 column 2) |
| {"a": 1 | After a complete value inside an object | Expected ',' or '}' after property value in JSON at position 7 (line 1 column 8) |
| [1, 2 | After a complete element inside an array | Expected ',' or ']' after array element in JSON at position 5 (line 1 column 6) |
| {"a": "abc | Inside a string — a closing quote is owed | Unterminated string in JSON at position 10 (line 1 column 11) |
| {"a" | After a property name — a colon is owed | Expected ':' after property name in JSON at position 4 (line 1 column 5) |
| 1. | Inside a number — digits are owed | Unterminated fractional number in JSON at position 2 (line 1 column 3) |
7 of these produce the generic message and 6 produce a positioned one. Taking one realistic payload and cutting it in different places shows the same split:
{"id": 1, "name": "ada", "tags": ["x", "y"]}
Cut it in different places and the message changes:
{"id": 1, "name": -> Unexpected end of JSON input
{"id": 1, "name": "ada" -> Expected ',' or '}' after property value
{"id": 1, "name": "ad -> Unterminated string in JSON at position 21
{"id": 1, "tags": [ -> Unexpected end of JSON input
{"id": 1, "tags": ["x", -> Unexpected end of JSON input
{"id": 1, "tags": ["x", "y" -> Expected ',' or ']' after array elementThese strings come from the versions listed at the end. Other engines and older releases word things differently, and Python does not produce this sentence at all — the rule described here is V8's behaviour, not a property of JSON.
When the response body is empty
The single most common origin: an endpoint that returns 200 with nothing in the body, and a caller that hands it straight to response.json(). A handler that saves a record and returns without writing a response does exactly this.
| Scenario | What you get | Note |
|---|---|---|
| 200 with an empty body | Unexpected end of JSON input | The endpoint returned nothing. The most common single cause. |
| 200 with a whitespace-only body | Unexpected end of JSON input | Indistinguishable from empty as far as the parser is concerned. |
| Truncated JSON body | Depends where it was cut | Sometimes this message, often a positioned one. See the table above. |
| 204 No Content | Unexpected end of input | Note the wording: no "of JSON". A different string, easy to conflate. |
| HTML error page | Unexpected token '<' | A different problem entirely — see the guide linked below. |
| Body already read | TypeError: body stream already read | A TypeError, not a SyntaxError. Calling .json() or .text() twice. |
Two of these are worth reading carefully, because they are routinely listed as causes of this error and are not. A 204 produces Unexpected end of input — no of JSON — and reading the body twice produces a TypeError about the body stream rather than a syntax error at all.
When the response is truncated
A body that starts correctly and stops mid-payload usually means something upstream gave up: a handler that timed out, a process that crashed while streaming, a proxy or gateway limit, or a connection closed early. The client cannot recover the missing bytes, and no parser setting will help.
Whether you see this message or a positioned one depends purely on where the stream stopped, which is why two failures of the same endpoint can report differently. Both mean the same thing about your system.
Diagnosing the actual body
Read the body once as text and look at it before parsing. That separates the three cases — empty, truncated, and not-JSON-at-all — in one step:
// Read the body once, as text, and look at it before parsing.
const res = await fetch(url);
const body = await res.text();
console.log(res.status, res.headers.get('content-type'), body.length);
console.log(JSON.stringify(body.slice(-40))); // how does it end?
if (body.length === 0) {
// Empty body. Check the status: 204 and many 304s have no body by design.
} else {
JSON.parse(body);
}- 1. Measure the body length before parsing itZero means empty, and the question becomes why the endpoint returned nothing. Non-zero means the payload is incomplete, and the question becomes where it was cut off.
- 2. Look at the last few characters, not the firstThis error is about how the input ends. Printing the final characters shows immediately whether the payload stops after a colon, after a comma, or mid-string — which is what decides the message you got.
- 3. Check the status code and content typeA 204 has no body by design, and calling .json() on it is a bug in the caller rather than in the server. A content type of text/html means something else went wrong upstream.
- 4. Rule out reading the body twiceCalling .json() after .text(), or .json() twice, raises a TypeError about the body stream rather than a SyntaxError. If the error names a stream, this is the cause.
- 5. For genuinely truncated payloads, look at the producerA response cut mid-payload usually means a timeout, a crashed handler, a proxy limit, or a stream closed early. The JSON is a symptom; nothing in the client can reconstruct the missing bytes.
Checking the payload
Once you have the raw body, pasting it into the JSON Formatter settles which case you have. Empty or whitespace-only input is reported as Input is empty., which answers the question directly. Anything else is parsed by the same engine your browser uses, so you get V8's message and — where V8 provides one — its position, marking how far the payload got.
It cannot fetch the response, does not know the status code, and will not repair a truncated payload or invent the missing bytes. It tells you what the text you have actually is. Parsing runs in your browser, so a payload containing real data is not uploaded anywhere.
Related messages
| Message | Means |
|---|---|
| Unexpected token '<' | An HTML page arrived instead of JSON |
| Unexpected end of input | A different string — often a 204, or a JS syntax error |
| Unterminated string in JSON at position N | Cut inside a string value |
| Bad control character in string literal | A raw newline or tab inside a string |
The first is the one most often confused with this error, and it is a different problem: the response was an HTML page rather than JSON, so JSON never arrived at all rather than arriving incomplete. If instead your payload is complete but contains a raw newline or tab inside a string, that is a control-character problem.
Versions measured
Error wording and the boundary described here are implementation details that change between releases. Every string on this page was produced by:
- Node.js / V8 —
24.16.0 / 13.6 - Chrome (same V8 family) —
148 - DataToolsHQ JSON Formatter —
native JSON.parse (V8)
Both engines tested agreed on every case. This is not a claim about every runtime or every version — only about what current V8 does, and why.