“Unexpected token … in JSON at position N”

The most common error in JavaScript, and the least informative. This page explains what position N is, how to find it, and works through every cause — from an HTML error page arriving instead of JSON, to a trailing comma, to a byte order mark you cannot see. Paste the document below and the tool names the cause and points a caret at it.

Indent

Paste the raw response, credentials and all. There is no upload endpoint, no storage and no share link on this page — the parser runs in your tab. A debugging payload full of tokens goes no further than your own browser. Why that is worth checking on any tool you use →

What the message actually says

Three things, and only three:

  1. Which character surprised the parser. The “token” named in the message.
  2. Where it was. Position N, counted in characters from zero, across the entire document including newlines — not a line number, and not an offset within a line.
  3. That the parser had no legal way to continue. Nothing about why, and nothing about where the real mistake is.

That last point is the one that wastes the most time. The position is where the parser noticed the problem, which is frequently not where you made it. A missing comma is reported at the value that follows the gap. An unclosed brace is reported at the end of the file, possibly thousands of characters away. An unclosed string swallows everything after it, so the error lands at whatever character finally violated the string rules.

Rule of thumb: when the reported position looks perfectly fine, the mistake is before it. Read backwards from the caret, not forwards.

The same error, worded four ways

The wording depends entirely on which JavaScript engine you are running, and V8 changed its messages substantially in 2023. Recognising the dialect tells you where you are:

EngineTypical messageGives you
V8, older
Chrome < 118, Node < 20
Unexpected token } in JSON at position 27 Character offset only
V8, current
Chrome, Node, Edge, Deno
Expected property name or '}' in JSON at position 27 (line 3 column 5) Offset and line/column, and a much better description
SpiderMonkey
Firefox
JSON.parse: expected property name or '}' at line 3 column 5 of the JSON data Line and column, no offset
JavaScriptCore
Safari, Bun
JSON Parse error: Expected '}' Often no position at all

Current V8 also emits several messages that are not “unexpected token” at all and are far more diagnostic when you read them carefully:

  • Unexpected token 'o', "[object Obj"... is not valid JSON — you passed an object, not a string. See cause 13 below.
  • Unexpected end of JSON input — the document is empty or truncated. Cause 2, and a full page on every trigger.
  • Unterminated string in JSON at position N — a quote was opened and never closed. Cause 10.
  • Unexpected non-whitespace character after JSON at position N — two documents concatenated. Cause 9.
  • Bad control character in string literal in JSON at position N — a real newline or tab inside a string. Cause 11.

How to find position N

The offset is only useful once you can turn it into a place in the file. Four ways, cheapest first:

  1. Paste the document into the box above. You get the line, the column, the character index and the offending line with a caret under it. That is what this page is for.
  2. Slice it in the console. Given the raw text in a variable:
    console.log(JSON.stringify(text.slice(N - 40, N + 40)));
    The JSON.stringify wrapper matters — it renders a carriage return as \r, a tab as \t and a byte order mark as \ufeff, instead of printing them as nothing at all.
  3. Convert the offset to a line number.
    const line = text.slice(0, N).split('\n').length;
    const col  = N - text.lastIndexOf('\n', N - 1);
    console.log(`line ${line}, column ${col}`);
  4. Jump to the byte in an editor. In Vim, :goto N. In VS Code, the status bar shows the offset when you enable it, or use Ctrl/G after converting to a line with the snippet above. Careful: N counts UTF-16 code units, so a document containing emoji or other astral-plane characters will drift from a byte-based editor offset.

And before any of that, print the raw text. Not the parsed object, not a variable your framework has already processed — the exact string you handed to JSON.parse:

const text = await response.text();
console.log(response.status, response.headers.get('content-type'));
console.log(JSON.stringify(text.slice(0, 200)));
const data = JSON.parse(text);   // now the failure is explainable

In more than half of real cases this single change ends the investigation, because it reveals that the body was never JSON to begin with.

The causes, in the order you should check them

1. The response is HTML, not JSON

Message: Unexpected token < in JSON at position 0, or in current V8, Unexpected token '<', "<!DOCTYPE "... is not valid JSON.

Position 0 with a < is unambiguous: the very first character was an angle bracket, so the body is HTML or XML. Something between your code and the API returned a page instead of data:

  • A 404 or 500 page rendered by the web server, not the application.
  • A 502/504 from a load balancer, reverse proxy or CDN.
  • A login redirect — the session expired and the framework served the sign-in page with a 200 status.
  • A captive portal on hotel or conference wifi intercepting the request.
  • A dev-server fallback: an unmatched /api/... path falling through to index.html, which is why this is epidemic in single-page-app development.
  • A WAF or bot-protection interstitial.

Every one of these, with worked examples and a full checklist, is covered on the dedicated page for this exact message.

Fix: check the status and content type before parsing, and log the body when the check fails.

const res = await fetch(url);
const body = await res.text();
if (!res.ok || !(res.headers.get('content-type') || '').includes('json')) {
  throw new Error(`Expected JSON, got ${res.status} ${res.headers.get('content-type')}: ` +
                  body.slice(0, 200));
}
return JSON.parse(body);

2. The body is empty

Message: Unexpected end of JSON input, with no position at all — the parser ran out of input before it found anything.

Causes: a 204 No Content response (correct behaviour for a successful DELETE, and it has no body by definition); a 304 Not Modified; a HEAD request; a body already consumed by an earlier .text() or .json() call (a fetch Response body can only be read once); or a request that failed before the server wrote anything.

Fix: treat an empty body as a valid outcome rather than a parse failure.

if (res.status === 204 || res.headers.get('content-length') === '0') return null;
const text = await res.text();
return text ? JSON.parse(text) : null;

3. A trailing comma

Message: Unexpected token } in JSON at position N, or in current V8, Expected double-quoted property name in JSON at position N.

Invalid
{
  "host": "db.internal",
  "port": 5432,
}
Valid
{
  "host": "db.internal",
  "port": 5432
}

JSON has never allowed a comma after the last member. It feels legal because JavaScript, Python, Rust, Go and every modern linter allow it — specifically to keep diffs clean when a line is appended. JSON does not, and never will; the format is frozen by design.

Fix: delete the comma. If a template or string-concatenation loop is generating the JSON, stop doing that — build a value and serialise it with your language’s JSON library, which cannot produce a trailing comma.

4. Single quotes

Message: Unexpected token ' in JSON at position N.

Invalid
{'name': 'Ada'}
Valid
{"name": "Ada"}

JSON strings use double quotes, full stop. This usually arrives from a JavaScript object literal that was pasted rather than serialised, or from Python’s str(dict) / repr() output, which is not JSON even though it looks close.

Fix in Python: use json.dumps(d), never str(d). Going the other way, if you have already got a Python-repr string, ast.literal_eval parses it safely; eval does not, and never should be used on data from elsewhere.

5. Unquoted property names

Message: Unexpected token n in JSON at position N, or Expected property name or '}'.

Invalid
{name: "Ada", age: 36}
Valid
{"name": "Ada", "age": 36}

In JavaScript, {name: "Ada"} is a perfectly good object literal. In JSON, every key must be a double-quoted string, without exception — including keys that look like identifiers, and including numeric keys, which must be written "1".

6. NaN, Infinity, undefined, None, True

Message: Unexpected token N in JSON at position N, or in Safari, JSON Parse error: Unexpected identifier "NaN".

Not JSONWhere it comes fromWrite instead
NaNA float division that went wrong upstreamnull
Infinity, -InfinityOverflow, or a deliberate sentinelnull
undefinedJavaScript string concatenationnull
None, True, FalsePython str(dict)null, true, false
nilRuby, Go, Lispnull

JSON has no way to express not-a-number or infinity. Python’s json.dumps will happily emit NaN and Infinity by default, producing output that no JavaScript client can read. Pass allow_nan=False to make it raise instead, and decide explicitly what those values should become.

7. Comments

Message: Unexpected token / in JSON at position N.

JSON has no comment syntax — Douglas Crockford removed it deliberately, because people had started using comments to carry parsing directives. Neither //, /* */ nor # is legal.

Fix: if you control the format, use JSON5, JSONC (what VS Code settings files use), YAML or TOML. If you are stuck with JSON, some people add a "_comment" key — ugly, but valid. If you must strip comments programmatically, use a real tokenizer such as strip-json-comments or jsonc-parser; a regular expression will eat the // inside every URL in the file.

8. A byte order mark

Message: Unexpected token � in JSON at position 0, or an inscrutable message naming a character that is not visibly there.

A UTF-8 BOM is the three bytes EF BB BF at the start of a file, appearing as the single character U+FEFF. Windows tools write it by default: Notepad, PowerShell’s Out-File and > redirection, Excel’s CSV/JSON export, Visual Studio. It is invisible in every editor. It is legal in a UTF-8 file and rejected by every conforming JSON parser.

Fix:

# Python — the "sig" codec consumes a BOM if present, and tolerates its absence
data = json.load(open('file.json', encoding='utf-8-sig'))

# JavaScript
if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1);

# Shell — check for one
head -c 3 file.json | xxd        # "efbbbf" means there is a BOM

# Editors: VS Code status bar → "UTF-8 with BOM" → switch to "UTF-8"

Paste a BOM-prefixed document into the tool above and it names it explicitly rather than pointing at a character you cannot see.

9. Two documents concatenated

Message: Unexpected non-whitespace character after JSON at position N, or Unexpected token { in JSON at position N where N is just past a closing brace.

{"id": 1, "event": "login"}
{"id": 2, "event": "logout"}

That is NDJSON, also called JSON Lines: one complete document per line. It is an excellent format for logs and streaming, and it is not valid JSON, deliberately — a consumer can handle each line as it arrives without waiting for a closing bracket that may be gigabytes away.

Fix: parse line by line.

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

Or, to inspect the whole file in the tool above, wrap it: '[' + lines.join(',') + ']'.

The same message with no newline between the documents usually means a response was retried and both attempts were written to the same buffer, or two log writers shared a file descriptor without locking.

10. A truncated document

Message: Unexpected end of JSON input, or Unterminated string in JSON at position N.

The document simply stops. Causes are almost always about transport rather than syntax: a stream consumed before it finished; a fixed-size read buffer; a socket timeout mid-body; a curl interrupted; a database column too narrow (a VARCHAR(255) holding a JSON blob is a classic); a log line cut by a line-length limit; or a Content-Length that disagrees with what was actually sent.

Fix: compare the length of what you received against the Content-Length header, and check whether the last character is the closing bracket you expect. The tool above tells you how many brackets are still open, which is a good signal of how much is missing.

11. A real newline or tab inside a string

Message: Bad control character in string literal in JSON at position N, or Unexpected token in JSON at position N.

Invalid
{"note": "line one
line two"}
Valid
{"note": "line one\nline two"}

JSON has no multi-line string. Every character below U+0020 must be escaped: \n, \r, \t, \b, \f, or the general \u00XX form. This shows up whenever JSON is assembled by string concatenation and a value happens to contain a newline — a user comment, a stack trace, a multi-line address.

Fix: stop concatenating. JSON.stringify escapes control characters correctly and takes no thought. If you have a broken document already, the escaping page converts raw text into a correct string literal.

12. A stray backslash — usually a Windows path

Message: Bad escaped character in JSON at position N, or Unexpected token U in JSON at position N.

Invalid
{"path": "C:\Users\new\test.txt"}
Valid
{"path": "C:\\Users\\new\\test.txt"}

Inside a JSON string a backslash always begins an escape sequence, and only \" \\ \/ \b \f \n \r \t \uXXXX are legal. \U is not an escape, so the parser rejects it. Worse, \n in \new is valid and silently becomes a newline, so a path can parse successfully and be wrong.

The same trap catches regular expressions ("\d+" must be "\\d+"), LaTeX and Windows UNC paths.

13. You passed an object, not a string

Message: Unexpected token 'o', "[object Obj"... is not valid JSON — the giveaway is the letters [object Obj in the message.

JSON.parse coerces its argument to a string first. An object becomes "[object Object]", whose second character is o. So the error is telling you that you parsed something already parsed.

Fix: most HTTP clients parse for you. fetch does not (await res.json() does), but Axios, jQuery and most SDKs return a parsed object. Calling JSON.parse on that is the mistake. Similarly, JSON.parse(JSON.parse(x)) is only correct if x is genuinely double-encoded — which happens more often than it should when a JSON string is stored in a JSON field.

14. Double-encoded JSON

A related shape worth recognising:

"{\"id\": 1, \"name\": \"Ada\"}"

That is a valid JSON string whose contents happen to be JSON. Parsing it once gives you the inner text, not an object. This happens when a producer calls json.dumps twice, when a JSON document is stored in a string column and re-serialised on the way out, or when a message queue wraps the payload.

Fix: parse twice if the value really is double-encoded, but fix the producer — double encoding wastes bytes, defeats schema validation and confuses every consumer. The escaping page unwraps one layer so you can see what is inside.

15. Numbers that are not JSON numbers

InvalidWhyWrite instead
01234Leading zeros are forbidden"01234"
.5Must start with a digit0.5
5.A decimal point needs a digit after it5.0
+5No leading plus5
0x1FNo hexadecimal31
1_000No digit separators1000
10nNo BigInt literal"10"

The leading-zero rule catches real data constantly: zip codes, phone numbers, ISBNs, account numbers, German postcodes. None of these are quantities — you never add two zip codes — so they belong in strings, which also protects them from being reformatted or losing their leading zero somewhere downstream.

16. Characters that look right but are not

Text that has been through a word processor, a PDF, a chat client or a corporate email system may contain characters that render like ASCII and are not:

  • Curly quotes U+201C and U+201D instead of ". Word, Google Docs, Slack and iOS substitute these automatically.
  • Non-breaking space — U+00A0 instead of U+0020. Common in text copied from HTML and from PDFs.
  • Zero-width space U+200B, invisible entirely, often introduced by copy-paste from a rendered web page.
  • Ideographic space U+3000, from CJK input methods.
  • An en dash where a minus sign belongs.

The tool above names the exact code point when it finds one of these, which is the difference between a five-second fix and half an hour of staring at a line that looks perfect.

Quick reference

What you seeMost likely cause
Unexpected token < at position 0HTML error page or login redirect, not JSON
Unexpected end of JSON inputEmpty body (204/304) or truncated response
Unexpected token } / Expected double-quoted property nameTrailing comma
Unexpected token 'Single-quoted string — JavaScript or Python repr
Expected property name or '}'Unquoted key, or a trailing comma
Unexpected token N / Unexpected identifier "NaN"NaN, None, True, undefined
Unexpected token /A comment
Unexpected token at position 0, invisible characterByte order mark
Unexpected non-whitespace character after JSONNDJSON, or two documents concatenated
Unterminated string in JSONTruncated, or an unescaped quote inside a value
Bad control character in string literalA real newline or tab inside a string
Bad escaped characterA Windows path or regex with single backslashes
Unexpected token 'o', "[object Obj"...You parsed something already parsed

How to stop hitting this error

  • Never build JSON with string concatenation or a template. Build a value in your language and serialise it. Every trailing-comma, quoting and escaping bug in this page disappears the moment you do.
  • Check the status and content type before parsing. Two lines that turn “unexpected token <” into “the API returned 502”.
  • Log the raw body on failure. Wrap JSON.parse in a helper that catches, includes the first two hundred characters of the input in the error, and rethrows.
  • Validate JSON files in CI. jq empty over every .json file in a pre-commit hook costs milliseconds.
  • Use a format that allows comments if you want comments. Fighting JSON’s strictness in a hand-edited config file is a losing game; JSON5, JSONC, YAML and TOML all exist for that reason.

Frequently asked questions

Is position N a line number?

No. It is a character offset from the start of the document, counting from zero and including every newline. On a minified single-line document position 41,208 is genuinely 41,208 characters in. Paste the document above to get a line and column instead.

Why does the error point at a character that looks correct?

Because the parser reports where it could no longer continue, not where the mistake was made. A missing comma is detected at the start of the next value; an unclosed brace at the end of the file; an unclosed string at whatever character finally broke the string rules. Always read backwards from the reported position.

Position 0 — but the file looks like it starts with a brace.

Then there is something invisible before it. In order of likelihood: a UTF-8 byte order mark, a zero-width space, or leading whitespace that is actually U+00A0. Run head -c 3 file.json | xxd — if it prints efbbbf, that is your BOM. Pasting the file above will name whichever it is.

My JSON works in Postman but fails in code. Why?

Postman shows you the response it received after following redirects and with its own headers, including an Accept header that may make the server answer differently. Your code may be hitting an authentication redirect that Postman’s stored cookie avoids, or requesting without Accept: application/json and getting the HTML representation of the same resource. Log the status, the content type and the first 200 characters of the body from the failing code path — not from Postman.

Can I just make the parser more forgiving?

You can — JSON5, json5 in npm, or Python’s demjson will accept comments, trailing commas and single quotes. Think carefully about where the leniency lives. Accepting sloppy input from a config file a human maintains is reasonable. Accepting it from a network peer means your service and its neighbours now disagree about what a valid message is, which is precisely the kind of parser mismatch that turns into a security advisory.

Why does Python produce JSON that JavaScript cannot read?

Two reasons. str(dict) and repr(dict) are not JSON — they use single quotes and None/True. Always use json.dumps. And json.dumps itself emits NaN, Infinity and -Infinity for those float values by default, which no JavaScript client accepts. Pass allow_nan=False to make it raise instead of emitting something unreadable.

Does an unclosed string really cause an error far away?

Yes, and it is the most misleading case there is. A missing closing quote means the parser reads everything after it — braces, commas, other keys — as ordinary characters inside one very long string. The error surfaces at the first character that violates a string rule, such as a raw newline, or at the end of the file. If the reported position makes no sense at all, look for an unbalanced quote well before it.

Is it safe to paste a failing production payload into this page?

Into this one, yes: it has no upload endpoint, no storage and no share link, and everything runs in your tab. That is not true of hosted formatters generally — in November 2025 two of the most popular ones were found to have been exposing years of stored submissions, credentials included. The details, with sources.

Related pages

Related tools