Skip to content
DevTinyTools

Errors

Unexpected end of JSON input: what it means

This error fires when the input ends at a point where a value was still due. Everything else people assume about it — that the JSON was cut off, that bytes are missing from the end — describes only three of six truncations, and none of the cases that actually get reported.

8 min readTool: JSON Validator

What the message actually says

The wording is precise and almost everybody reads it wrong. It does not say “your JSON is incomplete”. It says the parser reached the end of the input while it was still expecting a value — which is a statement about the parser's state at the moment it gave up, not about the document.

A JSON parser moves through a small number of states: expecting a value, expecting a property name, expecting a separator, inside a string. When the input runs out, the message you get depends entirely on which state it was in. That is why the same damage — bytes missing from the end — produces four different sentences, and why this particular one is so often a red herring.

Everything below was produced by running the inputs through three parsers on this machine: Node 22.22.2 (V8), Python 3.13.12 (json) and MySQL 8.4.3 (CAST … AS JSON). The messages are copied character for character, not paraphrased.

Six truncations, three engines

Every row is a document that was cut short. If the message were a diagnosis, all six would say the same thing. They do not: only the first three produce the sentence you searched for.

The same six truncated inputs run through V8, Python and MySQL
InputV8PythonMySQL
(empty)
Nothing arrived at all
Unexpected end of JSON inputExpecting value: line 1 column 1 (char 0)"The document is empty." at position 0.
Whitespace arrived, but no value
Unexpected end of JSON inputExpecting value: line 1 column 4 (char 3)"The document is empty." at position 3.
{"a":
Cut off immediately after the colon
Unexpected end of JSON inputExpecting value: line 1 column 6 (char 5)"Invalid value." at position 5.
{
Cut off immediately after the opening brace
Expected property name or '}' in JSON at position 1 (line 1 column 2)Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"Missing a name for object member." at position 1.
[1,2
Cut off inside an array
Expected ',' or ']' after array element in JSON at position 4 (line 1 column 5)Expecting ',' delimiter: line 1 column 5 (char 4)"Missing a comma or ']' after an array element." at position 4.
"abc
A string that never closes
Unterminated string in JSON at position 4 (line 1 column 5)Unterminated string starting at: line 1 column 1 (char 0)"Missing a closing quotation mark in string." at position 4.

Read the V8 column again. The first three rows are the message you came for. Rows four, five and six are equally truncated — a brace with nothing after it, an array with no closing bracket, a quote with no closing quote — and none of them produces it. If you are hunting for missing bytes at the end of a document, this message is weak evidence.

Why only the first three

The dividing line is not how much input is missing. It is whether the parser still wanted a value.

  • An empty input never read a token, so the very first thing it wanted was a value. End of input, expecting a value.
  • Whitespace is discarded before parsing, so a string of spaces is the same situation as an empty one. This is worth knowing because a trailing newline from a text editor looks harmless and behaves identically.
  • {"a": read a property name and a colon, so the next thing required is a value. End of input, expecting a value.
  • { is different: after an opening brace the parser wants a property name or a closing brace, not a value. Different state, different sentence.
  • [1,2 already has a complete element, so what is required next is a comma or a closing bracket.
  • "abc is inside a string literal, which has its own dedicated message in both V8 and MySQL.

So the rule of thumb is narrow but reliable: this message means the input stopped at a point where a value was due. If a value was not due, truncation announces itself with a different message, and you should stop looking for the wrong thing.

The usual cause is not truncation at all

Because the message fires when a value is due and never arrived, the most common real-world trigger is a response body of length zero rather than a half-written document. Four situations account for most reports:

  • A 204 No Content answer, or any endpoint that returns a success status with an empty body. Networking libraries hand you an empty string, and parsing it fails exactly here.
  • A proxy, gateway or CDN that returned its own response and dropped the payload. The request may even be logged as successful on both ends.
  • A body that was already consumed. Calling response.json() and then reading the response again yields nothing the second time, and the second parse sees an empty string.
  • A JSON document assembled by string concatenation where a variable was still undefined, leaving a hole at the point a value should have been.

None of these is a syntax error in your data. In every case the parser is correctly reporting that it was handed nothing when it was promised something.

Where the three engines disagree

The table above is worth reading for a second reason: the columns do not merely use different words, they point in different directions. Three cases make that concrete.

Inputs where the three parsers disagree about more than wording
InputWhat differsDetail
PositionV8 reports no position at all. Python counts to column 4 and MySQL to position 3 — both pointing past the whitespace, at the place the value should have been.
"abcDirectionV8 and MySQL point at the end of the input (position 4). Python points at the start of the string (column 1). One tells you where it stopped, the other where it began.
{"a":VocabularyV8 says the input ended. MySQL says “Invalid value.” Python says it was expecting a value, which is the same thing but names the missing part instead of the absence.

The practical consequence: any code that pattern-matches on error text to decide what went wrong is only valid for one parser. A regular expression tuned to V8 will not fire on Python, and the unterminated-string case will point at the wrong end of the document even when it does.

How to find out what actually arrived

Read the body as text before parsing it. Length and first bytes answer the question in one step, and they answer it for every one of the six cases above.

const res = await fetch(url);

// Read text first. Cloning is only needed if something else also reads the body.
const text = await res.text();
console.log(res.status, text.length, JSON.stringify(text.slice(0, 40)));

if (text.length === 0) {
  throw new Error("Empty body - not a JSON problem");
}

// A byte-order mark survives text() and breaks JSON.parse one character later.
const body = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;

try {
  return JSON.parse(body);
} catch (error) {
  throw new Error(`Body starts with ${JSON.stringify(body.slice(0, 20))}`, {
    cause: error,
  });
}

Three lines of logging distinguish every case in the first table. A length of zero is the empty-body case. A length that matches what DevTools shows but a first character of < is an HTML fallback. A first character of \uFEFF is a byte-order mark, which produces a different message entirely.

The fix that holds

There are two separate fixes because there are two separate problems, and applying the wrong one is why this error keeps coming back.

  • If the body is empty, the fix is on the sending side: return a status that says so, or send "null" explicitly. A client cannot distinguish “no data” from “failed silently” when both arrive as zero bytes.
  • If the body was consumed, clone before inspecting. Reading a response body is a one-shot operation, and a debugging line added in the wrong place will empty it for the code that follows.
  • If the document really was cut off, expect a different message. Handle every parser error by its shape rather than its wording: whether a position is reported, whether the input was quoted back, and whether the failure happened at the first character or later.

One habit prevents most of this class of bug: validate before you parse, using the same input the parser will see. Paste the body into a JSON validator and the difference between an empty body, an HTML page and a truncated document becomes visible immediately, without a round trip through your application logs.

Related reading

If the first character is an angle bracket rather than nothing, the input was not truncated — it was a different document. The position 0 guide covers the SPA fallback, the 404 page and the byte-order mark that produce it, and the guide to parser messages covers the messages that arrive with a position attached. If the failure only appears after a round trip through a database column, the JSON5 guide explains the comma and comment complaints.

Frequently asked questions

What causes “Unexpected end of JSON input”?
The parser reached the end of the string while it still expected a value. In practice that is almost always an empty response body — a 204, a proxy that dropped the payload, or a fetch call whose body was never read — rather than a JSON document that was cut off mid-way.
Does this message mean my JSON was truncated?
Not necessarily, and usually not. Three other kinds of truncation produce completely different messages: cutting a document after the opening brace reports a missing property name, cutting inside an array reports a missing comma or bracket, and cutting inside a string reports an unterminated string. Only an input that ends where a value was required produces this exact wording.
Why does the same input give a different message in Python?
Each parser reports its own internal state, not a shared diagnosis. For a whitespace-only input, Python names the column after the whitespace while V8 names nothing at all. For an unterminated string, V8 points at the end of the input and Python points at the start of the string.
How do I see what the server actually sent?
Read the response as text before parsing it, and log its length and first bytes. A body of length zero explains this message immediately; a body starting with an angle bracket is an HTML page; a body starting with a byte-order mark will fail for a different reason again.
Why does this error appear when the response looks fine in DevTools?
The usual reason is that the body was consumed twice. Calling response.json() and then trying to read the body again yields nothing, and the second parse receives an empty string. Clone the response before inspecting it.