← All articles

JSON Parse Errors: Unexpected Tokens, End of Input, Commas, Quotes, and HTML

Diagnose JSON.parse errors by the input you actually received, including HTML, undefined, objects, trailing commas, bad escapes, control characters, truncated JSON, and extra data after a valid document.

JSON.parse() is strict on purpose. It does not parse "almost JSON", JavaScript object literals, JSONC config, HTML error pages, or a value that used to be an object before something turned it into "[object Object]".

An “Unexpected token” error indicates that the parser has seen a character that is not valid at that point in the JSON grammar. The token tells you what the parser actually got. It tells you where to look; the position, line or column. Debug those two facts first before guessing at the API or changing the parser.

Read the Token First

Take the first surprise token as a shortcut. In real projects, reading the whole payload gets you to the right bug slower than this table.

Token or message What usually happened First place to check
< The response is HTML, not JSON. Usually a 404 page, login redirect, proxy error, or SPA fallback route. Network tab, status code, content-type, request URL, auth cookies.
u at position 0 You passed undefined or an unset value. Function arguments, state initialization, environment variables, optional cache reads.
o at position 1 A plain object was coerced to "[object Object]". Code that calls JSON.parse() on an object or sends body: payload instead of JSON.stringify(payload).
' The input uses single quotes. Copied JavaScript object literal, Python str(dict), hand-edited config.
} or ] after a comma The input has a trailing comma. Last item in an object or array.
/ The input contains // or /* */ comments. JSONC config pasted into a strict JSON parser.
T, F, N, or another identifier The input uses True, False, None, NaN, Infinity, undefined, or another non-JSON literal. Python output, JavaScript debug output, LLM-generated snippets.
Invisible character at position 0 A byte-order mark or control byte is before the first visible character. File encoding, copied text, CLI output.
Unexpected end of JSON input The parser ran out of text before the object, array, or string closed. Empty response, truncated stream, missing bracket. See Unexpected end of JSON input.
Non-whitespace after JSON data There is extra text after a complete JSON value. Logs pasted with JSON, two JSON documents concatenated, NDJSON parsed as one value.

Different engines phrase the error differently:

SyntaxError: Unexpected token '<', "<html>..." is not valid JSON
SyntaxError: Unexpected token u in JSON at position 0
SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
SyntaxError: JSON Parse error: Single quotes (') are not allowed in JSON

Don’t overfit to the specific wording. Chrome, Node.js, Firefox and Safari are not using the same messages. Check the input and consider the message a hint.

Log What You Actually Parsed

Most time wasted debugging json is spent assuming the input is the value you wanted to parse. Make a safe preview log before fixing anything.

function previewForJsonParse(value) {
  const text = typeof value === 'string' ? value : String(value);

  return {
    type: typeof value,
    length: text.length,
    firstChar: JSON.stringify(text[0] ?? ''),
    preview: JSON.stringify(text.slice(0, 160)),
  };
}

console.log(previewForJsonParse(raw));

That preview catches several problems immediately:

  • type: "undefined" means you never had JSON text.
  • preview: "\"[object Object]\"" means an object was coerced before parsing.
  • preview: "\"<!DOCTYPE html>...\"" means your API returned HTML.
  • firstChar: "\"\\ufeff\"" means a BOM is at position 0.
  • length: 0 means you are parsing an empty string, not a malformed object.

For production code, return a parse result instead of letting a random component crash:

function safeJsonParse(text) {
  try {
    return { ok: true, value: JSON.parse(text) };
  } catch (error) {
    return {
      ok: false,
      error: error instanceof SyntaxError ? error.message : String(error),
    };
  }
}

Use this pattern at the edge of your app: form imports, local storage, feature flags, clipboard paste, dev tools. For internal API contracts it is usually better to fail loudly and fix the producer.

If the Token Is <, Your Fetch Returned HTML

Unexpected token '<' is the classic fetch failure. The body starts with < because the server returned an HTML document:

  • /api/user is misspelled and returned the app shell.
  • The user is logged out and got a login page.
  • A reverse proxy returned a branded 502 page.
  • A serverless route crashed and returned an HTML error page.
  • A static host rewrote the API path to index.html.

This helper reads the body once, checks the status, checks the content type, and adds a small preview in the error:

async function readJsonResponse(response) {
  const text = await response.text();
  const contentType = response.headers.get('content-type') || '';

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`);
  }

  if (!contentType.includes('application/json')) {
    throw new Error(
      `Expected JSON, got ${contentType || 'no content-type'}: ${text.slice(0, 200)}`,
    );
  }

  try {
    return JSON.parse(text);
  } catch (error) {
    throw new Error(
      `Invalid JSON response: ${error.message}. Body starts with ${JSON.stringify(
        text.slice(0, 120),
      )}`,
    );
  }
}

Do not call await response.json() first when you are debugging this case. Once the stream is consumed, you lose the raw body unless you clone the response. Read text, inspect it, then parse.

If the Token Is u, You Parsed undefined

JSON.parse(undefined) does not receive a special JavaScript value. The algorithm converts the argument to a string first, so the parser sees "undefined" and rejects the u.

JSON.parse(undefined);
// SyntaxError: Unexpected token u in JSON at position 0

Common causes:

  • A function argument was optional, but the parser assumes it is present.
  • A React/Vue/Svelte component parses data before async loading completes.
  • An environment variable is missing.
  • A cache wrapper returns undefined for a miss.
  • A property path is wrong: settings.userJson instead of settings.user.json.

Guard the source, not the parser:

function readConfig(raw) {
  if (typeof raw !== 'string' || raw.trim() === '') {
    return { theme: 'system', compact: false };
  }

  return JSON.parse(raw);
}

const result = readConfig(process.env.APP_SETTINGS_JSON);

One detail that surprises people: JSON.parse(null) returns null. That is because null is converted to the string "null", and "null" is valid JSON. undefined is not.

If the Token Is o, You Parsed an Object

JSON.parse() expects text. If you pass a plain object, JavaScript coerces it to "[object Object]". The parser accepts the first [ as the start of an array, then rejects the o at position 1.

const payload = { name: 'Ada' };

JSON.parse(payload);
// SyntaxError: Unexpected token o in JSON at position 1

Fix it by deleting the parse when you already have an object:

const payload = { name: 'Ada' };

// Already an object. Use it directly.
console.log(payload.name);

For requests, serialize before sending:

await fetch('/api/profile', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ name: 'Ada' }),
});

If your log contains the literal text "[object Object]", a repair tool cannot recover the original keys and values. The data was lost earlier. Fix the code path that produced the string. For a deeper look at serialization boundaries, see How to stringify JSON.

If the Token Is a Quote, Comma, or Slash, You Have "Almost JSON"

Many unexpected token errors come from syntax that is valid somewhere else, just not in strict JSON.

Single quotes are valid in JavaScript strings, not JSON strings:

JSON.parse("{'name': 'Ada'}");
// SyntaxError

JSON.parse('{"name": "Ada"}');
// OK

Unquoted keys are valid in JavaScript object literals, not JSON:

JSON.parse('{name: "Ada"}');
// SyntaxError

JSON.parse('{"name": "Ada"}');
// OK

Trailing commas are valid in modern JavaScript and many config formats, not JSON:

JSON.parse('{"name": "Ada", "score": 98,}');
// SyntaxError near the closing brace

JSON.parse('{"name": "Ada", "score": 98}');
// OK

Comments are valid in JSONC, not JSON:

JSON.parse(`{
  // internal note
  "name": "Ada"
}`);
// SyntaxError near /

Avoid quick regex comment strippers unless they are string-aware. This breaks real data:

const raw = '{"url": "https://example.com/a//b"}';

// Bad idea: this removes part of the string value.
raw.replace(/\/\/.*$/gm, '');

Use a JSONC parser for config files on purpose. Fix it. Validate it as strict JSON. For pasted or one-off input. Check the output.

If the Token Is T, F, N, I, or Another Identifier

Strict JSON has only three literal words: true, false, and null. They are lowercase.

These are not JSON:

JSON.parse('{"enabled": True}');
JSON.parse('{"enabled": False}');
JSON.parse('{"value": None}');
JSON.parse('{"value": undefined}');
JSON.parse('{"ratio": NaN}');
JSON.parse('{"limit": Infinity}');

Fix the producer where possible:

  • In Python, use json.dumps(data), not str(data).
  • In JavaScript, use JSON.stringify(data), not template strings or object interpolation.
  • For LLM output, ask for strict JSON and still validate the response.
  • For NaN and Infinity, decide whether the value should become null, a string sentinel, or a domain-specific error.

JSON.stringify() converts NaN, Infinity, and -Infinity to null in object values. That can be acceptable for charts, but it is a lossy decision. Do not let it happen silently in finance, permissions, quotas, or audit logs.

If Position 0 Looks Fine, Check Invisible Characters

Sometimes the first visible character is {, but the parser reports an unexpected token at position 0. Inspect the character code instead of trusting your eyes.

function inspectFirstCharacter(text) {
  const char = text[0] ?? '';

  return {
    visible: JSON.stringify(char),
    codePoint: char ? `U+${char.codePointAt(0).toString(16).toUpperCase()}` : null,
  };
}

A UTF-8 byte-order mark appears as U+FEFF. Strip it only at the start of the document:

const clean = raw.replace(/^\uFEFF/, '');
const data = JSON.parse(clean);

Do not remove every control character blindly. Tabs, newlines, and carriage returns are legal whitespace between JSON tokens, and escaped versions such as \n can be meaningful inside strings. The next section separates raw control characters from broken escapes and incomplete strings.

Map a Position to Line and Column

V8-style errors usually include position 123. That position is a zero-based character index into the string. Convert it to line and column before you hand the bug to another person.

function describeJsonParseError(text, message) {
  const match = String(message).match(/position (\d+)/);
  if (!match) return null;

  const position = Number(match[1]);
  const before = text.slice(0, position);
  const line = before.split('\n').length;
  const column = before.length - before.lastIndexOf('\n');

  return {
    position,
    line,
    column,
    character: JSON.stringify(text[position] ?? ''),
    context: JSON.stringify(text.slice(Math.max(0, position - 30), position + 30)),
  };
}

Use JSON.stringify() around the character and context when logging. It makes quote characters, tabs, newlines, BOMs, and other invisible details visible.

If Parsing Ends Too Soon, Check Empty and Truncated Input

Unexpected end of JSON input means the parser needed another token but reached the end of the string. The missing token is often a closing }, ], or quote, but the input may also be completely empty.

JSON.parse('');                    // empty input
JSON.parse('{"user":{"id":7}'); // missing }
JSON.parse('{"name":"Ada}');     // missing closing quote

In fetch code, an empty body can be legitimate. 204 No Content, some 304 responses, and endpoints that deliberately return no representation should not be sent to response.json().

async function readJsonOrNull(response) {
  if (response.status === 204 || response.status === 205) return null;

  const text = await response.text();
  if (!text.trim()) return null;

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${text.slice(0, 160)}`);
  }

  return JSON.parse(text);
}

If a non-empty response ends halfway through a value, do not automatically add a bracket. A truncated transfer may also have lost fields or array items. Compare content-length where available, inspect proxy and server logs, and retry the request. When reading a stream, buffer a complete JSON document or use an explicit framed format; arbitrary network chunks are not parseable documents.

Trailing Commas Are Invalid JSON

A trailing comma is a comma immediately before a closing brace or bracket:

{
  "name": "Ada",
  "roles": ["admin", "editor",]
}

There are two errors here: the comma after "editor" and the comma that would follow the last object property if one were present. JavaScript object and array literals allow trailing commas, but JSON does not. The safe correction is structural:

{
  "name": "Ada",
  "roles": ["admin", "editor"]
}

Avoid a global /,\s*([}\]])/ replacement in production. It can rewrite comma-like text inside strings and hides a broken producer. Use a JSON-aware repair tool for pasted input; use JSON.stringify(), a standard library serializer, and a strict parse check in CI for generated files.

Distinguish Bad Escapes, Control Characters, and Unterminated Strings

These messages all point into a JSON string, but they describe different defects:

Error family Invalid example Correct form
Bad escaped character {"path":"C:\new\q"} Escape backslashes: {"path":"C:\\new\\q"}
Bad Unicode escape {"mark":"\u12G4"} Use exactly four hexadecimal digits after \u
Raw control character A literal newline or tab inside the quotes Use \n or \t in JSON text
Unterminated string {"name":"Ada} Add the missing quote only after confirming the value was not truncated

JSON recognizes \", \\, \/, \b, \f, \n, \r, \t, and \uXXXX. An unknown sequence such as \q is invalid. A literal line break is also invalid inside a quoted JSON string even though an escaped \n represents the same character after parsing.

When JSON is embedded in a JavaScript string, remember that two grammars consume escapes. This JavaScript source needs doubled backslashes to deliver a JSON escape to JSON.parse():

const text = '{"message":"first\\nsecond"}';
console.log(JSON.parse(text).message);

If the string is from a failed transfer or cut-off log, then repair can’t determine the missing text. Do not fabricate a closing quote. Either reject it or get it back.

If Valid JSON Is Followed by More Data

Messages such as unexpected non-whitespace character after JSON data mean the parser successfully finished one value and then found another character:

{"id":1}{"id":2}
{"id":1} request completed

Do not discard everything after the first close brace. Input can be two concatenated records, JSON interspersed with logs, or NDJSON (one JSON value per line). Parse NDJSON into records:

function parseNdjson(text) {
  return text
    .split(/\r?\n/)
    .filter((line) => line.trim() !== '')
    .map((line, index) => {
      try {
        return JSON.parse(line);
      } catch (error) {
        throw new Error(`Invalid NDJSON record ${index + 1}: ${error.message}`);
      }
    });
}

Use a real boundary for streaming protocols: newline-delimited JSON, a length prefix, or some other documented frame. Concatenated objects without delimiter are ambiguous and should be fixed on the producer.

Decide Whether to Repair or Reject

Not all unexpected tokens should be corrected. The JSON repair step helps if the input is human generated or exploratory. It is dangerous when the input represents a contract, a permission, a payment or a destructive action.

Situation Good response
A user pasted approximate JSON into a developer tool Repair, format, then show the result for review.
An LLM returned almost-JSON Repair only as a cleanup step, then validate required keys and types.
Your own API returned malformed JSON Reject the response and fix the server or serializer.
Fetch returned HTML Fix the URL, auth, route, proxy, or content type. Do not repair HTML into JSON.
A repository config file has comments or trailing commas Use JSONC intentionally, or enforce strict JSON in CI with jq empty file.json.
A financial, permission, migration, or deletion workflow receives invalid JSON Reject it, log the raw preview, and require a valid producer.

That distinction is what keeps a repair tool useful without hiding upstream bugs.

Use JSON Fix for Pasted Input

If you have a broken JSON sample and need a clean version quickly, paste it into JSON Fix. It can repair common syntax issues such as single quotes, trailing commas, unquoted keys, comments, Python literals, undefined, markdown fences, and unclosed structures. The repair runs in your browser, so the text does not need to leave your machine.

After repair, do one more pass:

  • Format the output and confirm the shape.
  • Compare before and after with JSON Diff if the data matters.
  • Validate the final text with strict JSON parsing.
  • Use schema validation for API payloads, imports, and configuration.

Frequently Asked Questions

What does "Unexpected token in JSON" mean?

JSON.parse() found a character that is not valid at that point in the JSON grammar. The named token tells you what the parser actually received, and the position tells you where to inspect the input.

Why does "Unexpected token <" happen with fetch?

The response body starts with HTML, usually a 404 page, login redirect, proxy error, or front-end fallback route. Read the response as text, check response.ok and content-type, and fix the endpoint before parsing.

How do I fix "Unexpected token u in JSON at position 0"?

You passed undefined or an unset value to JSON.parse(). Guard the value before parsing and initialize state, function arguments, localStorage fallbacks, or environment variables deliberately.

Why does JSON.parse({}) cause token o or [object Object]?

JSON.parse() expects text. A plain object is coerced to the string [object Object], so the parser rejects the o in object. Use the object directly, or call JSON.stringify() when you need JSON text.

How do I find the exact position of the error?

If the engine reports a position, then slice the string at that index and map it to line and column. When you log, string the snippet so that invisible characters and quote characters are visible.

Can JSON.parse() handle comments or trailing commas?

No. Comments and trailing commas are valid in some JavaScript and JSONC contexts, but not in strict JSON. Remove them Use a JSONC parser for config files Fix input before strict parsing.

Should I auto-repair unexpected token errors?

Repair is OK for pasted examples, developer tools, and LLM output after review. Reject malformed JSON Fix the producer for API contracts, financial actions, permission changes or destructive workflows.

Why do Chrome, Firefox, and Safari show different JSON.parse errors?

They have different message wording and javascript engines. Is the payload really JSON ? Debug from the actual token . Use exactly the text as a clue . Input preview . Line or column .

What causes "Unexpected end of JSON input"?

The parser ran out of input before the value was complete. Look for an empty response. Missing closing brace, bracket, or quote. A truncated transfer or code that parses a stream before the full document arrives.

How do I fix an unterminated string or bad escaped character?

Check the source near the reported position. Make sure the value was not truncated Escape backslashes and control characters properly Add a trailing quote. For machine generated data, use a fixed serializer rather than patching strings with regex.

Why is there extra data after valid JSON?

Maybe you have concatenated documents, JSON + log text, or NDJSON. Know the framing format and parse each record carefully; do not just silently throw away the rest.

Sources

Last reviewed August 2026.