What “end of input” means, precisely
Every other JSON error names a character that broke a rule. This one is different: the
parser reached the last character it was given while still expecting more — a closing
} or ] that never arrived, a string with no closing quote, or
simply nothing at all. Per
RFC 8259,
an empty string is not a valid JSON document under any interpretation — a document
must contain exactly one value, and zero characters contain no value. That is why an empty
response body is the single most common trigger, not an exotic edge case.
The wording differs across engines more than for most JSON errors, because there is no specific character to name:
| Engine | Message |
|---|---|
| V8 — Chrome, Node, Edge, Deno | Unexpected end of JSON input |
| SpiderMonkey — Firefox | JSON.parse: unexpected end of data at line 1 column 1 of the JSON data |
| JavaScriptCore — Safari, Bun | JSON Parse error: Unexpected EOF |
All three describe the identical situation: reached the end, wanted more. The causes and fixes below apply regardless of which one you are looking at.
The causes, most common first
1. An empty response body
The single most frequent trigger. JSON.parse('') throws this exact error in
every engine — an empty string simply has no value in it. This happens when:
- A
204 No Contentresponse is handed to.json()anyway. A 204 has no body by definition; that is correct HTTP behaviour, not a bug in the server. - A
304 Not Modifiedis treated the same way — also bodyless by design. - A
HEADrequest response is parsed as if it were aGET. - The server returned
200with a genuinely empty body, which usually indicates a bug on the server side — a handler that returned before writing anything.
Fix: treat “no body” as a valid, distinct outcome rather than routing it through the parser at all.
if (res.status === 204 || res.status === 304) return null;
const text = await res.text();
if (text.length === 0) return null;
return JSON.parse(text);
2. A truncated or streamed response cut short
The document was genuinely on its way and stopped: a connection dropped mid-transfer, a
proxy or load balancer enforced a timeout while the body was still streaming, a client
library’s read buffer was smaller than the payload, or a server crashed after writing
headers but before finishing the body. The result is a document that starts correctly and
simply stops — often mid-string or mid-object, which is why this same message can also
arrive as Unterminated string in JSON at position N depending on exactly where
the cut happened to land.
Fix: compare the number of bytes actually received against the
Content-Length header if one was sent, and treat a mismatch as a transport
failure worth retrying rather than a data problem worth debugging. Paste a truncated document
into the tool above and it reports how many brackets are still open, which tells you roughly
how much is missing.
3. A response body read twice
A fetch Response body is a stream, and a stream can only be
consumed once. Calling .text() and then .json() on the same
response — directly, or indirectly because a logging wrapper, an interceptor or a retry
helper already read the body once — usually throws its own distinct error
(“body stream already read,” a TypeError, not this
SyntaxError). The case that does produce this exact message is subtler:
something upstream partially drains the stream — a middleware that peeks at the first
chunk for logging or content-sniffing — and hands the remainder downstream. The second
reader sees a real but incomplete document and reports precisely this error.
Fix: read the body exactly once, and if you need it in two places, clone
the response first — res.clone().json() gives a second independent reader
over the same underlying data.
4. An unclosed string or bracket in a hand-built document
Rather than a transport problem, the document itself is simply incomplete: a
{ with no matching }, an array missing its closing
], or a string missing its closing quote so that everything after it —
commas, braces, the rest of the document — is swallowed into one long unterminated
string. This is common in JSON built by string concatenation or a template where a closing
character was dropped, and in hand-edited config files.
// Missing closing brace
{"host": "db.internal", "port": 5432
// Missing closing quote — everything after this is now part of the string
{"note": "unfinished
Fix: paste the document above; the tool reports exactly which brackets are still open and where the string that never closed began.
5. A response size limit or timeout cutting the body off
A related but distinct cause from truncation over a flaky connection: a deliberate server-side or gateway-side limit — a maximum response size, a request timeout, a serverless function’s execution limit — can cut a response off partway through writing a large document. Whereas cause 2 is usually incidental (a dropped connection), this one is a policy: the response was going to be cut regardless, once it passed a size or time threshold. The fix is on the server side — stream large responses, paginate, or raise the limit — rather than in the parsing code.
6. JSON.parse(undefined) — a common assumption that turns out
wrong
It is easy to assume that calling JSON.parse with no usable input at all
produces this exact message. It does not, in any current engine tested directly:
| Call | Chrome/Node (V8) | Firefox | Safari |
|---|---|---|---|
| JSON.parse('') | Unexpected end of JSON input | unexpected end of data… | Unexpected EOF |
| JSON.parse(undefined) | "undefined" is not valid JSON | unexpected character… | Unexpected identifier "undefined" |
The reason is that JSON.parse coerces its argument to a string first, and
String(undefined) is the five-character text "undefined" —
not empty. The parser has plenty of (invalid) input to look at, so it reports an unexpected
token, not an unexpected end. Where this genuinely bites: JSON.parse(localStorage.getItem('key'))
when the key does not exist. getItem returns null, and
JSON.parse(null) actually succeeds — String(null) is the
valid JSON literal "null", which parses to the value null. The bug
that actually produces this page’s error is almost always an empty
string reaching the parser (cause 1), not a missing value in the JavaScript sense.
Worked example
A DELETE handler that returns 204 on success is called from
client code that unconditionally parses every response as JSON:
// Fails on every successful delete
async function deleteItem(id) {
const res = await fetch(`/api/items/${id}`, { method: 'DELETE' });
return res.json(); // throws: Unexpected end of JSON input
}
The handler is behaving correctly — a 204 has no body, per the HTTP specification — and the bug is entirely in the client assuming every response has one:
async function deleteItem(id) {
const res = await fetch(`/api/items/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
if (res.status === 204) return null;
const text = await res.text();
return text ? JSON.parse(text) : null;
}
A second worked example: a size limit cutting a stream mid-document
A background job downloads a large report from an internal service through a corporate API gateway. Small reports succeed; once the report grows past a certain size, the same code fails with exactly this error, deterministically:
const res = await fetch(reportUrl);
const text = await res.text();
console.log(text.length, res.headers.get('content-length'));
// 1048576 3812004
const data = JSON.parse(text); // Unexpected end of JSON input
The received length (1,048,576 — exactly 1 MiB) is far short of the
Content-Length the server declared (3,812,004). A gateway in front of the
service was silently capping response bodies at 1 MiB and closing the connection, rather
than returning an error status. Nothing in the JSON itself was wrong; the transport
truncated a correct document partway through. The fix here was entirely infrastructural
— raising the gateway’s body-size limit and having the upstream service paginate
large reports instead of returning them as one document — and no amount of parser
code could have worked around it. Comparing the two lengths, as in the snippet above, is
what turned a confusing parse error into an obvious, five-minute infrastructure fix.
How to stop hitting this error
- Never call
.json()unconditionally. Check the status first; 204 and 304 are correct, bodyless outcomes, not failures. - Read a response body exactly once. If more than one part of the code needs it, clone the response before either reads it.
- Compare received length against
Content-Lengthwhen a response is unusually large, to catch silent truncation early rather than downstream in a parser error. - Never build JSON with string concatenation. A dropped closing brace or quote in hand-assembled JSON is entirely avoidable by serialising a real value with your language’s JSON library instead.
- Log the byte length and the first and last 100 characters on any parse failure. A truncated document almost always ends mid-string, mid-number or mid-key; an empty one is zero characters long; both are obvious the moment you look, and neither is obvious from the error message alone.
- Set an explicit timeout you control rather than relying on a platform default, so a slow response is aborted cleanly with a network error instead of being cut off silently mid-body by whatever infrastructure sits between you and the server.
Quick reference
| What you see | Most likely cause |
|---|---|
| Unexpected end of JSON input, res.status === 204 | Bodyless success parsed anyway |
| Unexpected end of JSON input, empty text | Zero-length body from a 200 that should not be empty |
| Unterminated string in JSON at position N | Truncation or double-read landed inside a string |
| Length far short of Content-Length | Transport truncation — timeout, size cap, dropped connection |
| "undefined" is not valid JSON | Not this error — a missing value that was never a string at all |
| "body stream already read" | Not this error either — a full double-read, a distinct TypeError |
Limitations and edge cases
- The message gives you no position. Unlike most JSON errors, there is no character to point at — the document ended, which by definition has no “where.” The tool above compensates by reporting which brackets are still open rather than a single offset.
- Whitespace-only input triggers the same message.
JSON.parse(' ')is treated identically to an empty string in every engine tested — whitespace has no value inside it either. - A single unterminated string can produce either this message or “Unterminated string in JSON at position N,” depending on exactly where the string started relative to the end of the document. Both point at the same root cause: a missing closing quote.
- Streaming APIs (NDJSON, Server-Sent Events) are not affected by this at all when handled correctly — they are designed to be read incrementally, line by line, rather than parsed as one document. This error usually means a streaming response was parsed as if it were a single, complete JSON document instead.
Frequently asked questions
Why is there no position number in this error?
Because the parser did not find a character that broke a rule — it ran out of characters while still expecting one. There is nothing to point a caret at. Some engines report a line and column anyway (always line 1, column 1 for a genuinely empty string), which tells you the same thing in a different shape: the document ended immediately.
Is a 204 response actually an error?
No — a 204 No Content is a successful response
that is, by the HTTP specification, defined to have no body. Calling
res.json() on it is the bug, not the 204 itself. Check the status before
parsing and treat 204 as a valid, bodyless success.
Does JSON.parse(undefined) cause this error?
No, in every current engine tested directly it throws a different
message — "undefined" is not valid JSON in Chrome/Node, an
“unexpected character” message in Firefox, and
“Unexpected identifier "undefined"” in Safari — because
JSON.parse first stringifies its argument, and String(undefined)
is the non-empty text "undefined". This error is almost always an empty
string, not a missing value.
My response worked yesterday and fails today with no code changes. Why?
Suspect the transport, not the code: a proxy or CDN change, a newly introduced response-size limit, a timeout that used to be generous enough and no longer is, or an upstream service that started returning 204 or an empty body under a condition your client never previously hit. Compare the raw response — status, headers, and the first and last characters of the body — against a known-good capture from before the change.
Can I read a response body more than once safely?
Yes, with res.clone() called before either read.
Cloning gives you two independent readers over the same underlying data, so a logging
step and the real parse can both consume it in full. Reading the same, uncloned response
object twice is what throws the “body stream already read” error, and a partial
read followed by a second read on the same object is what produces this page’s
truncated-document version.
Is it safe to paste a failing production payload into this page?
Into this one, yes: there is no upload endpoint, no storage and no share link, and every check runs in your tab. That is not true of hosted formatters generally — in November 2025 two of the most widely used online code-formatting utilities were found to have been exposing years of stored submissions, credentials included. The details, with sources.
Does a size limit really cut a response off mid-document rather than returning an error?
Yes, and it is one of the least intuitive causes on this page
because nothing about the HTTP exchange looks wrong — the status is often still
200. A gateway, load balancer or CDN enforcing a maximum body size can close
the connection once that limit is reached regardless of whether the upstream service was
still writing, leaving the client holding a well-formed prefix of a document and nothing
else. Comparing the received length against the Content-Length header, as in
the worked example above, is the fastest way to confirm this rather than an ordinary
dropped-connection truncation.
Related pages
- “Unexpected token” explainedEvery cause of the most common JSON error
- “Unexpected token < … position 0”Your API returned HTML, not JSON
- “JSON.parse: unexpected character”The Firefox and Safari wording, decoded
- JSON validatorCheck syntax and get the exact line and column