Javascript is required
TroubleshootingPublished 2026-07-2810 min read

JSON.parse Unexpected Token: Debugging Causes

When JSON.parse throws “Unexpected token”, “unexpected character”, or “bad parsing”, the exact message is engine-dependent. The durable questions are: what input reached the parser, where did parsing stop, and what token was expected there? This guide uses reproducible input categories instead of unsafe, one-size-fits-all repair regexes.

By: Tiny
JSONJSON.parseSyntaxErrorUnexpected token调试

1. Confirm that the input is JSON text

JSON.parse coerces non-string inputs. Passing an object that was already parsed can become “[object Object]” and fail near the beginning. If fetch response.json() or a framework already returned an object, do not parse it again.

function parseJsonText(input: unknown): unknown {
  if (typeof input !== 'string') {
    throw new TypeError(`Expected JSON text, received ${typeof input}`);
  }
  return JSON.parse(input);
}

2. Eight common causes, grouped by source

First decide whether the payload is not JSON at all or is JSON-shaped text with invalid syntax. That distinction prevents changes at the wrong layer.

  • HTML error, login, proxy, or WAF page returned instead of JSON.
  • An object is parsed a second time.
  • Single-quoted strings or unquoted property names.
  • Trailing commas or JavaScript-style comments.
  • Unescaped newlines, tabs, quotes, or backslashes inside strings.
  • Invalid numbers such as NaN, Infinity, 01, 1., or hexadecimal literals.
  • Empty or truncated response bodies.
  • A BOM, multiple root values, or extra non-whitespace text after the root value.

3. Inspect HTTP status, Content-Type, and raw body first

During diagnosis, read response.text() once and then validate it. Content-Type is useful but not conclusive because servers can mislabel HTML as JSON.

async function fetchJson<T>(url: string): Promise<T> {
  const response = await fetch(url, {
    headers: { Accept: 'application/json' },
  });
  const type = response.headers.get('content-type') ?? '';
  const text = await response.text();

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`);
  }
  if (!type.includes('application/json')) {
    throw new Error(`Expected JSON, got ${type || 'unknown type'}`);
  }
  if (text.trim() === '') {
    throw new Error('Expected JSON, got an empty body');
  }

  return JSON.parse(text) as T;
}

4. JSON is stricter than a JavaScript object literal

JSON requires double-quoted property names and strings, rejects trailing commas and comments, and has a limited number grammar. Fix the producer instead of guessing with broad text replacements.

const invalid = [
  "{'name':'Tiny'}",
  '{name:"Tiny"}',
  '{"name":"Tiny",}',
  '{"enabled":true // comment\n}',
  '{"value":NaN}',
  '{"value":01}',
];

for (const sample of invalid) {
  try {
    JSON.parse(sample);
  } catch (error) {
    console.log((error as Error).message);
  }
}

5. Extract context when an error position is available

V8 often reports a character position, while other engines may report line and column or a different message. Treat position parsing as an optional diagnostic enhancement and print escaped context so invisible characters become visible.

function diagnoseJson(text: string) {
  try {
    return { ok: true as const, value: JSON.parse(text) };
  } catch (error) {
    const message = (error as Error).message;
    const match = message.match(/position\s+(\d+)/i);
    const position = match ? Number(match[1]) : undefined;
    if (position === undefined) return { ok: false as const, message };

    const start = Math.max(0, position - 30);
    const end = Math.min(text.length, position + 31);
    return {
      ok: false as const,
      message,
      position,
      context: JSON.stringify(text.slice(start, end)),
    };
  }
}

6. Generate JSON with JSON.stringify

Manual concatenation breaks as soon as user content contains quotes, backslashes, or line breaks. Build an object first and let JSON.stringify apply the required escaping. Embedded JSON strings need one stringify/parse operation per representation layer; see /articles/nested-json-escaping.

const input = 'line 1\nline 2 "quoted" C:\\temp';
const text = JSON.stringify({ message: input });
const parsed = JSON.parse(text) as { message: string };
console.assert(parsed.message === input);

7. Why generic auto-repair is unsafe

Removing all backslashes, replacing every single quote, or deleting comments with regex can alter legitimate string data. Use a JSON5 or JSONC parser only when that input format is explicitly part of the contract, then convert at the boundary.

  • Do not parse a 204 No Content response as JSON.
  • An empty string is not JSON; use null for an explicit empty value.
  • A response contains one root JSON value, not concatenated objects.
  • Handle BOMs or other prefixes only after confirming their source.

8. Verification, FAQ, and conclusion

Preserve the raw payload, classify the source, reproduce with the smallest failing text, inspect context, fix the producer, and keep the original bad sample as a regression test. The homepage formatter at / can help validate non-sensitive text.

Why do browsers show different messages? The language requires a SyntaxError but does not standardize the full wording.

Does “Unexpected token <” always mean a 404? No. It usually indicates HTML, which may be a login page, proxy response, or server error template.

try...catch contains the failure; it does not repair invalid JSON. The reliable fix is to identify whether the defect belongs to transport, call flow, or JSON generation.

Sources and further reading

Specifications and references for checking the article’s technical details.

Related articles

Keep exploring

Use the related browser tool to test the workflow with your own sample data.

Open related tool