Fix “Unexpected end of JSON input” Error in JavaScript

JSON.parse() is the standard way to turn a JSON string into a JavaScript object, and one of its most common failures is:

SyntaxError: Unexpected end of JSON input

It shows up in the browser, in Node.js, and in build tools, and it almost always means the same thing: the string you asked it to parse is empty, or it was cut off before reaching a complete JSON value. This guide covers every common cause and how to fix each one.

What the error actually means

JSON.parse() reads a string character by character and builds up a value. When it reaches the end of the string while still expecting more content — for example, it read an opening brace but never found the matching closing brace — it throws Unexpected end of JSON input.

In other words, the JSON is real but incomplete, which is different from parsing a non-JSON string (like HTML).

A useful way to think about it: the parser is not saying “this is not JSON.” It is saying “this was going to be JSON, but it stopped too early.” That distinction matters, because the fix is usually on the data-source side rather than in your parsing code.

The most common causes

1. Parsing an empty string

JSON.parse(''); // SyntaxError: Unexpected end of JSON input

An empty string contains no JSON value at all. This usually happens when a variable is unexpectedly an empty string, null, or undefined.

const raw = localStorage.getItem('config'); // null if the key was never set
JSON.parse(raw); // throws

localStorage.getItem() returns null — not undefined — for missing keys, and people often forget this. Note that JSON.parse(null) actually returns null without throwing, because null is coerced to the string "null", which is valid JSON. It is the empty string that blows up. This subtle difference is worth testing in your own console, because it explains why the same code sometimes throws and sometimes does not.

2. Truncated data from a file or API

If a download or response is interrupted, you end up with half a JSON document:

// Intended: {"name":"Tom","age":30}
const broken = '{"name":"Tom","age":30'; // missing closing brace
JSON.parse(broken); // throws

Truncation is the single most common real-world cause. A response that gets cut off mid-transfer, a file read that was interrupted, a database column with a length limit that silently clipped the JSON — all of them produce this exact error.

3. Reading a fetch response incorrectly

const res = await fetch('/api/user');
const text = await res.text();
JSON.parse(text); // throws when text is incomplete or HTML

If the request failed at the network level, or the server returned an error page, text will not be JSON at all. See the dedicated section below, because fetch has its own wrapped version of this error.

4. String manipulation dropped the closing brace

Slicing, trimming, or concatenating strings can accidentally remove the last characters:

const full = '{"status":"ok"}';
const cut = full.slice(0, -1); // '{"status":"ok"'
JSON.parse(cut); // throws

This is easy to spot in a short example and hard to spot in a 200-line function that builds a payload piece by piece.

Failed to execute 'json' on 'Response'

If you search for the exact error text, you probably saw this longer version:

Failed to execute 'json' on 'Response': Unexpected end of JSON input

This is the same underlying problem, surfacing through the fetch API. When you call await res.json(), the browser reads the response body and runs the JSON parser on it. If that body is empty or incomplete, the same Unexpected end of JSON input is produced, but the browser wraps it in a TypeError that names the method and the object it was called on.

There are four situations that produce it:

1. The response has an empty body

A 204 No Content response, a 204-style delete endpoint, or an API that returns nothing on success all give you an empty body. Calling .json() on it fails immediately — there is nothing to parse.

const res = await fetch('/api/items/42', { method: 'DELETE' });
const data = await res.json(); // throws: the body is empty

The HTTP status tells you this in advance: 204, 205, and any 3xx redirect mean there is no body worth parsing.

2. The response is HTML, not JSON

This happens constantly in real applications: the API path was wrong and you got the SPA’s index.html; the auth token expired and you got a login page; the server crashed and you got a 500 error page. The body is non-empty, so a length check passes, but the parser fails almost immediately.

const res = await fetch('/api/user');
console.log(res.headers.get('content-type')); // text/html

Checking content-type before parsing catches this case cleanly.

3. The body was already read

A response body is a stream, and a stream can only be consumed once. If some middleware, interceptor, or earlier line of code already called .json(), .text(), or .body.getReader(), a second call fails.

4. The response itself was cut off

A slow network, a proxy timeout, or a server crash mid-write produces a partial body. The parser gets a real prefix of real JSON and then hits the end.

The fix that covers all four:

async function readJson(res) {
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} ${res.statusText}`);
  }
  const type = res.headers.get('content-type') || '';
  if (!type.includes('json')) {
    throw new Error(`Expected JSON, got ${type || 'unknown'}`);
  }
  const text = await res.text();
  if (!text) {
    throw new Error('Empty response body');
  }
  try {
    return JSON.parse(text);
  } catch (e) {
    throw new Error(`Invalid JSON from ${res.url}: ${e.message}`);
  }
}

Reading as text first instead of calling .json() directly is the key move: it gives you the raw string, so you can log it, inspect its last characters, and produce an error message that actually tells you what went wrong.

Node.js scenarios

The same error appears in Node, but the sources are different from the browser’s.

fs.readFileSync returns a Buffer, not a string

const fs = require('fs');
const raw = fs.readFileSync('config.json'); // Buffer
JSON.parse(raw); // works by accident — Buffer coerces to a string

This usually works because JSON.parse coerces the Buffer to a string, but it breaks on files with a BOM or non-UTF-8 encoding. Be explicit:

const raw = fs.readFileSync('config.json', 'utf8');

require('./data.json') caches the file

require caches modules, so a second call returns the first parsed value. If the file changed on disk, you will still get the old content — and if the file was empty or half-written at the moment it was first required, you get this error and it looks impossible to reproduce. Use fs.readFileSync for data files that change at runtime.

Streams that have not finished

let raw = '';
stream.on('data', (chunk) => { raw += chunk; });
JSON.parse(raw); // runs before the stream ends -> empty or partial

The parse has to happen in the end handler, not after the on('data') registration.

A JSON file written concurrently

If a process is still writing the file while another reads it, you can read a valid JSON prefix and nothing else. Writing to a temp file and then renaming it atomically is the standard fix.

Library behavior: axios and jQuery

axios already parses JSON for you. response.data is the parsed object, not a string:

const res = await axios.get('/api/user');
const obj = JSON.parse(res.data); // throws — res.data is already an object

If res.data is already an object, JSON.parse will either throw Unexpected token o (it stringified to [object Object]) or, if the response really was empty, Unexpected end of JSON input. Either way the mistake is the same: double parsing. Check typeof res.data if you are unsure which layer did the parsing.

axios also sets transformResponse, which tries to parse JSON automatically and silently leaves the raw string in place if parsing fails — so you can end up with res.data being a string on failure and an object on success. Code that assumes one type breaks in exactly this scenario. Setting transformResponse: [] disables the automatic parsing and gives you predictable behaviour.

jQuery behaves differently across versions. Older $.ajax with dataType: 'json' parses for you and calls the error handler on failure instead of throwing; newer versions may resolve with a string. Explicitly setting dataType: 'text' and parsing yourself is the portable approach.

Empty or truncated? How to tell

These two causes need different fixes, and the error message is identical. Three checks separate them in seconds.

Check the length first.

console.log('length:', text.length);

Zero means empty — the problem is upstream (a 204, a missing body, a failed request). Non-zero means truncated — the problem is mid-transfer or mid-write.

Check the last characters.

console.log(JSON.stringify(text.slice(-40)));

Using JSON.stringify around the slice matters here: it escapes newlines and makes whitespace and invisible characters visible. Truncated JSON typically ends mid-string or right after a comma; complete JSON ends with a closing brace or bracket.

Compare against Content-Length.

const declared = Number(res.headers.get('content-length'));
const actual = text.length;
if (declared && actual < declared) {
  console.log(`Truncated: got ${actual} of ${declared} bytes`);
}

If the declared length is larger than what you received, the transfer really was cut short — that is a network or server problem, not a parsing problem.

How to fix it

Guard against empty input first:

function safeParse(str, fallback = null) {
  if (typeof str !== 'string' || str.trim() === '') return fallback;
  try {
    return JSON.parse(str);
  } catch (e) {
    console.warn('Invalid JSON:', e.message);
    return fallback;
  }
}

Returning a fallback rather than throwing keeps a single bad payload from taking down a whole page render.

Wrap parsing in try/catch so a bad payload cannot crash your app:

try {
  const data = JSON.parse(raw);
} catch (e) {
  console.error('Invalid JSON:', e.message, 'length:', raw.length);
}

Always log raw.length alongside the message. Without it, you cannot tell an empty payload from a truncated one after the fact.

Check the source is complete — verify the API returned a 200 status, the file finished downloading, or the response body is fully read before parsing.

Inspect the end of the string to confirm it ends with a closing brace or bracket:

console.log(raw.slice(-20));

Validate the shape, not just the syntax. A string can be perfectly valid JSON and still break your code — an object where you expected an array, a missing field that you then call a method on. If the data comes from an external source, validating the structure catches a whole class of bugs that JSON.parse never will.

Adjacent traps worth knowing

JSONL and NDJSON cannot be parsed as a single document. JSON Lines is one JSON value per line — a log format, not a document. JSON.parse on the whole file fails because after the first complete value there is more content. Split by newline and parse each line:

const records = text
  .split('\n')
  .filter((line) => line.trim())
  .map((line) => JSON.parse(line));

A BOM turns into Unexpected token at position 0. Files saved by some Windows tools start with a byte order mark (U+FEFF). It is invisible, it is not valid JSON, and the parser reports an error at the very first character. Strip it before parsing:

const clean = text.replace(/^\uFEFF/, '');

This usually shows up as Unexpected token rather than Unexpected end of JSON input, so it is a different error with a different cause — but it comes from the same family of “the file looks fine and the parser disagrees” problems.

Long numeric IDs change silently. JSON numbers are IEEE 754 doubles, which represent integers exactly only up to 2^53-1 (9007199254740991). A 19-digit Snowflake ID gets rounded, and the last digits change without any error at all. The fix belongs on the server side — serialise IDs as strings.

Pinpoint the exact break with a validator

Instead of staring at a long string, paste it into the JSON formatter. It parses the input immediately and reports the exact position where the JSON becomes invalid, so you can see precisely which character is missing. For the related error caused by HTML in your response, see Fix “Unexpected token < in JSON at position 0”.

Frequently asked questions

What does "Unexpected end of JSON input" mean?

It means JSON.parse() ran out of characters before it finished reading a complete JSON value. The string you passed was either empty or cut off in the middle, so the parser hit the end of the input instead of a closing brace or bracket.

Why does JSON.parse('') throw this error?

An empty string contains no JSON value at all, so the parser immediately reaches the end of the input and reports the error. Check whether your variable is empty, null, or undefined before parsing.

Is this the same as "Unexpected token < in JSON at position 0"?

No. That error means you parsed something that is not JSON (usually an HTML error page). "Unexpected end of JSON input" means the JSON was real but incomplete or empty.

What does "Failed to execute 'json' on 'Response'" mean?

It is the same underlying error surfacing through the fetch API. Response.json() tries to parse the response body, and if that body is empty, HTML, or partially read, it throws this wrapped version of the error. The fix is to check response.ok and read the body as text before parsing.

How do I find where my JSON got cut off?

Paste the string into a JSON validator to locate the exact position where parsing fails, then look at the data source — a truncated download, an incomplete API response, or a string operation that dropped a closing brace.

Why does a long numeric ID change after parsing?

JSON numbers are stored as IEEE 754 doubles, which can only represent integers exactly up to 2^53-1 (9007199254740991). A 19-digit Snowflake ID exceeds that range and gets rounded, so the last few digits silently change. Have the server return IDs as strings.