Unexpected End of JSON Input and How to Find the Cause
Unexpected end of JSON input means the text stopped too soon or was empty. Real messages for every cut point, and how to tell truncated from empty.

You call JSON.parse on a response and get SyntaxError: Unexpected end of JSON input. Unlike most parse errors, this one does not point at a bad character. It points at nothing, which is the clue: the parser read everything it was given and was still waiting for more. Either there was nothing to read, or there was not enough. The two causes have different fixes, and telling them apart takes one look at the length of the text. This article explains the message, shows exactly where the different truncation points land with real output from Node and Python, covers the usual sources of empty and cut-off text and gives the patterns that prevent them. It belongs to the family described in fixing JSON parse errors by reading the message properly.
What the parser is telling you
A JSON parser reads left to right and always knows what it can accept next: after { a string or }, after a key a colon, after a value a comma or a closing bracket. When the text ends before the document is complete, the parser is in the middle of a structure. The message says that, and different parsers and different versions word it differently.
The empty string is the cleanest case. JSON.parse("") raised Unexpected end of JSON input in Node, and json.loads("") raised Expecting value: line 1 column 1 (char 0) in Python. Both mean there is no value at all. For a truncated non-empty document, the message depends on where the cut fell, and here the newer engines are more precise than the old one-size-fits-all message. I cut the document {"id": 7, "name": "Ada", "tags": ["a", "b"], "ok": true} at every position and tallied the messages Node 24 produced:
15 cuts e.g. cut at 0 "" Unexpected end of JSON input
1 cuts e.g. cut at 1 "{" Expected property name or '}' in JSON at position N
24 cuts e.g. cut at 2 "{\"" Unterminated string in JSON at position N
4 cuts e.g. cut at 5 "{\"id\"" Expected ':' after property name in JSON at position N
4 cuts e.g. cut at 8 "{\"id\": 7" Expected ',' or '}' after property value in JSON at position N
6 cuts e.g. cut at 9 "{\"id\": 7," Expected double-quoted property name in JSON at position N
2 cuts e.g. cut at 37 "\"tags\": [\"a\"" Expected ',' or ']' after array element in JSON at position N
valid prefixes: []Only 15 of the 56 cuts gave the classic Unexpected end of JSON input: the empty text and the cuts that end where a value is expected, such as right after a colon or inside a literal like tru. A cut inside a string, which is most of this document (24 cuts), reported Unterminated string. A cut after a value reported what could follow it, a comma or a closing brace. So in a modern runtime, "unexpected end" is one of seven messages for a truncated body, and an error such as Expected ',' or '}' after property value at the last position of the text is the same problem. Python's tally for the same cuts was shorter:
15 cuts, e.g. at 0 '' Expecting value
7 cuts, e.g. at 1 '{' Expecting property name enclosed in double quotes
24 cuts, e.g. at 2 '{"' Unterminated string starting at
4 cuts, e.g. at 5 '{"id"' Expecting ':' delimiter
6 cuts, e.g. at 8 '{"id": 7' Expecting ',' delimiterSame 56 cuts, five messages. The last line of the Node output matters most: valid prefixes: []. No proper prefix of the document was valid JSON on its own, so a truncated object or array always fails. That is a property of the grammar: a closing bracket is required, and without it nothing parses. (The exception is a document that is a bare number or a literal: 12345 cut to 123 is valid and wrong, a reason to wrap data in an object or array and to check lengths for scalars.)
Is it empty or truncated
The one measurement that separates them is the length of the text. Log it, and the first and last 50 characters, when a parse fails. Empty means length 0 or only whitespace. Truncated means a plausible start and an end that stops mid-structure: the last characters are in the middle of a string or a list, and often there is no closing bracket.
For an HTTP response, compare with what the server promised. The Content-Length header says how many bytes to expect, and a body shorter than that was cut off in transit. With chunked encoding there is no length, and a connection that closed early shows up as a network error in many clients before the parse. Check the status code too. A 204 No Content has no body by definition, and calling .json() on it raises exactly this error. A 200 with a zero-length body is a server bug: an endpoint that has nothing to say for an empty result and writes nothing instead of [] or {}.
Where empty bodies come from
Several ordinary situations produce an empty string. 204 and 304 responses. The server replied that there is no content or that the cached copy is fine, and the client parses the empty body. Check res.status before res.json(). A request that was aborted or timed out, so that a wrapper returned an empty string instead of throwing. A body read twice: in fetch, a response body can be consumed once, and a second .json() or .text() reads from a closed stream and returns empty or throws a different error. An empty file: a job that created the output file and failed before writing, or two processes writing the same path. An uninitialized value: a variable assigned "" as a default, then parsed. A stored value that was cleared: reading localStorage.getItem(key) returns null for a missing key, which parses (to null), but an empty string stored earlier does not.
The defensive pattern is to make "no content" a handled case and not an exception:
const text = await res.text();
const data = text.trim() === "" ? null : JSON.parse(text);Decide what an empty body means for the call (no data, or an error) and handle it in one place.
Where truncation comes from
Truncation has a handful of causes, and each leaves a recognizable mark. A timeout or a dropped connection during a long response: the text is cut at an arbitrary point and Content-Length, if present, does not match. A size limit in a proxy, a log shipper or a message queue: the text is cut at exactly the limit, a round number like 1,048,576 bytes or 8,192 characters. If the length of failing texts is always the same, you have found a limit. A crash or kill while writing a file: the file ends mid-document, with no closing bracket, and may have zero length if it was created first. A buffer that was not flushed before the program exited. Streaming without a terminator: a producer that writes an array element by element and dies before the closing bracket. Truncated logs: a line limit cut a JSON line in half, and a log parser fails on it.
The same text read later may be complete, which makes truncation look like an intermittent bug. That is another reason to log the length and the tail on failure.
Preventing it
For files, write to a temporary name and rename when done. A rename within one file system is atomic, so a reader sees the old file or the complete new one and never a half-written one. The same pattern protects CSV exports (scheduling CSV reports from SQL with cron) and any consumer of files that appear in a directory. For responses, set Content-Length when you can, and check it on the client: a short body should be treated as an error and retried, not parsed. For streams of records, use JSON Lines: each line is a complete document, a truncated stream loses at most the last line and the reader can tell, because the last line lacks its newline.
For messages and payloads that pass through size-limited systems, validate the length before sending and split or compress large messages, and detect the limit on the receiving side by logging lengths. For clients, treat a parse failure on a short or empty body as a retryable network condition and log it as such, since retrying a truncated read often succeeds.
When the input is complete and still fails
Occasionally the text is complete and the message still says end of input. If you are parsing a prefix, a chunk of a larger stream (streaming parsers expect the rest), the message is accurate for that chunk. If the text was decoded from bytes with a replacement or a limit, trailing bytes may have been dropped. If the JSON contains a string with an unescaped quote near the end, the parser reads the rest as part of the string, runs off the end and reports an unterminated string, which looks like truncation, so check the last few lines for a stray quote. And if the document uses a backslash at the end of a string ("C:\temp\"), the final \" is an escaped quote and the string never closes.
Where none of this applies, bisect the text: parse the first half, then the first three quarters, and find the shortest prefix whose error message changes to "unterminated" or "expecting". The point at which it changes is the area to inspect. The parser's behavior is documented in MDN's JSON.parse page and the Python exception in the json module reference, and the specific case of an empty or HTML body at position 0 is in unexpected token in JSON at position 0.


