SyntaxError: Unexpected end of JSON input means the JSON parser ran out of text before it reached a complete JSON value. The input was empty, truncated, or missing a closing quote, bracket, or brace. It is not the same as Unexpected token <, Unexpected token u, or [object Object] is not valid JSON: those errors tell you the parser found the wrong character. This one usually means the parser needed more characters and there were none left.
In production JavaScript, the usual culprits are an empty HTTP response passed to response.json(), a copied JSON snippet that lost its last } or ], a network/proxy truncation, or code that tries to parse a stream chunk before the full message arrives.
Quick Triage
Start by logging safe metadata about the value right before parsing. Do not dump tokens or customer payloads into logs.
function describeJsonInput(input) {
const text = typeof input === "string" ? input : String(input ?? "");
return {
inputType: typeof input,
length: text.length,
startsWith: JSON.stringify(text.slice(0, 60)),
endsWith: JSON.stringify(text.slice(-60))
};
}
Then match what you see:
| Symptom | Likely cause | First fix |
|---|---|---|
length is 0 |
Empty string, empty HTTP body, blank form field, blank env var | Guard before parsing. |
HTTP status is 204, 205, or 304 |
Response intentionally has no body | Do not call response.json(); return null or a domain fallback. |
| Tail ends mid-key, mid-string, or mid-array | Truncated response or copied partial JSON | Inspect the raw response, network tab, proxy logs, or source file. |
Tail ends after {, [, ,, or : |
Missing closing structure or missing value | Repair or reject the malformed JSON. |
Happens with ReadableStream, SSE, or NDJSON |
Parsing chunks instead of complete messages | Buffer until a delimiter or full document boundary. |
Input starts with < |
HTML response, not this error family | Debug as Unexpected token <. |
Input is undefined |
Missing variable or missing return | Debug as Unexpected token u. |
That first log line often saves the whole investigation. If the length is zero, stop looking for a missing bracket. If the tail ends with "na, start looking for truncation.
What the Error Means
JSON.parse() parses one complete JSON value according to the JSON grammar. A complete value can be an object, array, string, number, boolean, or null, but it must be complete.
These all fail because the parser reaches the end too early:
JSON.parse("");
JSON.parse(" ");
JSON.parse("{\"name\":\"Ada\"");
JSON.parse("[1, 2, 3");
JSON.parse("{\"items\":[{\"id\":1},");
The parser may not give a useful position because the problem is missing text. There is no bad character to point at. It simply reached the end while still expecting a quote, value, ], or }.
Cause 1: Empty HTTP Response
This is the version that surprises people most:
const response = await fetch("/api/session/logout", { method: "POST" });
const data = await response.json(); // SyntaxError if the body is empty
Many endpoints intentionally return no JSON body:
204 No Content205 Reset Content304 Not ModifiedDELETEor logout endpoints that only signal success- feature-flag, health-check, or webhook endpoints that return an empty body
Fix it by treating "no body" as a valid outcome instead of forcing every response through JSON parsing.
async function readJsonOrNull(response) {
if ([204, 205, 304].includes(response.status)) {
return null;
}
const text = await response.text();
if (!text.trim()) {
return null;
}
const contentType = response.headers.get("content-type") || "";
if (!contentType.toLowerCase().includes("application/json")) {
throw new Error(`Expected JSON, got ${contentType || "no content-type"}`);
}
return JSON.parse(text);
}
That helper is intentionally boring. It distinguishes three different cases: no body, wrong content type, and malformed JSON.
Cause 2: Truncated API Response
Truncation means the producer intended to send complete JSON, but you received only part of it:
{"users":[{"id":1,"name":"Ada"},{"id":2,"na
Common causes:
- The network connection closed mid-body.
- A proxy, gateway, or serverless function hit a timeout.
- A CDN or reverse proxy buffered only part of a large response.
- The backend wrote JSON manually and crashed before writing the closing bytes.
- A log, clipboard, or ticket copied only the first part of the payload.
For fetch calls, collect status, content type, body length, and a tail preview. Avoid logging the full body unless it is safe.
async function fetchJsonWithDiagnostics(url) {
const response = await fetch(url);
const text = await response.text();
try {
return JSON.parse(text);
} catch (error) {
console.error("JSON parse failed", {
status: response.status,
contentType: response.headers.get("content-type"),
length: text.length,
tail: text.slice(-80)
});
throw error;
}
}
Content-Length can help, but use it carefully. It is a byte count, and it may be absent or not directly comparable when the response is compressed or chunked.
const expected = Number(response.headers.get("content-length"));
const actualBytes = new TextEncoder().encode(text).length;
if (Number.isFinite(expected) && actualBytes < expected) {
throw new Error(`Short JSON body: got ${actualBytes} of ${expected} bytes`);
}
If content-encoding is gzip, br, or deflate, or if transfer is chunked, compare logs from the server/proxy side instead of assuming the browser string length should match the wire length.
Cause 3: Missing Closing Brace or Bracket
Hand-edited JSON, generated examples, and copied snippets often lose the last closing delimiter:
JSON.parse("{\"name\":\"Ada\",\"plan\":\"pro\"");
JSON.parse("[1, 2, 3");
JSON.parse("{\"data\":{\"items\":[{\"id\":1}]");
Fix options depend on the source:
- If this came from a file you own, add the missing
}or]and validate it. - If this came from an API, reject it and fix the producer.
- If this came from a pasted snippet, run it through JSON Fix and review the repaired output.
- If this came from an LLM, repair syntax first, then validate required fields separately.
Auto-closing a bracket is convenient for developer tools. It is risky for business logic, because the missing tail may have contained fields you needed.
Cause 4: Unterminated String
A JSON string must close with a double quote. If the text ends while the parser is still inside a string, you get this error:
JSON.parse("{\"message\":\"upload started");
This often comes from:
- A copied log line cut off after the opening quote.
- A generated JSON response that stopped mid-token.
- A string value built by concatenation instead of
JSON.stringify(). - A stream parser reading before the full string arrived.
The safest fix is to stop building JSON text manually:
const payload = {
message: userMessage,
createdAt: new Date().toISOString()
};
const json = JSON.stringify(payload);
JSON.stringify() handles quotes, backslashes, tabs, and newlines inside the value. Manual concatenation does not.
Cause 5: Empty Form, Storage, or Environment Values
Empty strings are easy to create accidentally:
JSON.parse(""); // Unexpected end of JSON input
JSON.parse(" "); // Unexpected end of JSON input
Typical sources:
- A blank
<textarea>or hidden form field. process.env.FEATURE_FLAGS || "".- An empty file read from disk.
- A localStorage value initialized as an empty string.
- A database column that stores optional JSON as
""instead ofnull.
Guard before parsing:
function parseOptionalJson(text, fallback = null) {
if (typeof text !== "string" || !text.trim()) {
return fallback;
}
return JSON.parse(text);
}
One subtle JavaScript note: JSON.parse(null) returns null, because null is converted to the string "null" before parsing. Missing localStorage keys often return null, while empty saved values return "". Those two cases behave differently, so log the type as well as the length.
Cause 6: Parsing Stream Chunks Too Early
JSON is not self-healing when it arrives piece by piece. If you parse each chunk from a stream, you will eventually parse a partial object:
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
JSON.parse(chunk); // wrong: chunk may be half of a JSON document
}
Buffer until you have a complete message. For newline-delimited JSON, parse one line at a time:
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
handleEvent(JSON.parse(line));
}
}
}
if (buffer.trim()) {
handleEvent(JSON.parse(buffer));
}
If your API returns one big JSON document, wait for the whole body and parse once. If it returns NDJSON, SSE, or another framed format, parse at the frame boundary, not at the transport chunk boundary.
A Safer Fetch Pattern
For application code, return a structured result instead of letting parse errors escape without context:
async function requestJson(url, options) {
const response = await fetch(url, options);
const text = await response.text();
if (!text.trim()) {
return { ok: response.ok, status: response.status, data: null };
}
try {
return {
ok: response.ok,
status: response.status,
data: JSON.parse(text)
};
} catch (error) {
return {
ok: false,
status: response.status,
error: "invalid_json",
message: error instanceof Error ? error.message : String(error),
length: text.length
};
}
}
In API clients, you might throw instead of returning an object. The important part is the same: preserve the HTTP status and safe diagnostics so you can tell empty responses, HTML responses, and incomplete JSON apart.
Repair or Reject?
Not every Unexpected end of JSON input should be repaired.
| Context | Best response |
|---|---|
| User pasted a half-finished example into a tool | Offer repair and show the repaired output. |
| LLM returned JSON missing the final brace | Repair syntax, then validate required fields. |
Internal config file is missing ] |
Fail the build and fix the file. |
| API response from your backend is truncated | Reject it, log diagnostics, and fix the producer or network path. |
| Payment, permission, billing, or destructive action payload | Reject and retry safely; do not guess missing data. |
Repair is helpful for inspection. Contracted data should usually fail loudly.
How to Debug Without Leaking Data
When parse failures happen in production, log facts about the input, not the whole payload:
- HTTP method, URL route pattern, and status.
content-type,content-encoding, and safe length metadata.- Whether the body was empty or whitespace-only.
- The last 40 to 80 characters only if the data is known to be non-sensitive.
- A request ID that lets you correlate with server logs.
Do not log bearer tokens, cookies, passwords, private keys, customer records, or full webhook bodies just because a parser failed. Parse errors are debugging events, not permission to dump raw data.
Fix It Online
If the value is pasted text, an LLM response, or a copied fixture, use JSON Fix to repair and format it in your browser. Then open the result in JSON Viewer or validate it with JSON Validator.
For API contracts, use the online tool to inspect a sample, but fix the source system. A browser repair tool cannot prove that a truncated production response preserved all required fields.
Frequently Asked Questions
What causes "Unexpected end of JSON input"?
The parser reached the end of the input before a complete JSON value was closed. Common causes are empty strings, empty HTTP responses, truncated API bodies, missing } or ], unterminated strings, and parsing stream chunks too early.
Why does response.json() throw this error?
response.json() still has to parse the response body. If the body is empty, cut off, or not complete JSON, it throws a SyntaxError. For 204, 205, 304, and intentionally empty responses, check the status or text body before parsing.
How do I fix it for a 204 or empty response?
Do not call response.json() blindly. If the status means no body, return null or another domain fallback. If you read text first, treat empty or whitespace-only text as no JSON before calling JSON.parse().
How do I tell if the response was truncated?
Log the status, content type, body length, and a safe tail preview before parsing. If the tail ends mid-string, mid-key, after a comma, or inside an array, suspect truncation. Use server, proxy, and network logs to confirm.
Why is there no useful line or column number?
Because the problem is missing text. With an unexpected token, the parser can point at the bad character. With unexpected end, it ran out of characters while still expecting a quote, value, bracket, or brace.
Is an empty string valid JSON?
No. An empty string is not a JSON value. Valid empty-ish JSON values are null, "", [], and {}. If empty input is allowed in your app, handle it before parsing.
Can a JSON repair tool fix this?
Sometimes. A repair tool can add a likely missing closing brace or bracket for pasted examples, copied snippets, or LLM output. It should not be used to guess missing data in API contracts, payments, permissions, or other high-stakes workflows.
How do I handle this gracefully in production?
Wrap parsing in a helper that returns a fallback or structured error, and log safe diagnostics such as status, content type, length, and request ID. Do not log full sensitive payloads just to debug a parse failure.
Related Tools & Guides
- JSON Fix - repair and format invalid JSON
- JSON Validator - check strict JSON syntax and locate parser errors
- JSON Viewer - inspect repaired JSON as a collapsible tree
- Handling broken JSON in JavaScript - safe parsing and repair patterns
- Unexpected token < in JSON at position 0 - HTML response instead of JSON
- Unexpected token u in JSON at position 0 -
undefinedpassed to JSON.parse - Fix JSON unexpected token errors - lookup guide for related parser messages
Sources
- MDN: JSON.parse() - JavaScript JSON parsing behavior
- RFC 8259 - the JSON data interchange format
- MDN: Response.json() - fetch response body parsing
- MDN: Content-Length - response body byte length header
- MDN: ReadableStream.getReader() - streaming reader API
Last reviewed July 2026.