"Unexpected Token in JSON at Position N"

"Position 47" is not a line number, not a column, and — more often than people expect — not even the location of your actual mistake. Here's what the number really counts, why it tends to point one token past the real problem, and how to turn it into something you can actually navigate to.

reference guide JSON JSON.parse

// what "position" actually counts

The position is a plain character count from the start of the string you handed to JSON.parse — index 0 is the first character, and every character counts once, including spaces, tabs, and newlines. It is not a line number, and it doesn't reset at the start of each line. For a one-line JSON payload that distinction doesn't matter; the moment your JSON spans multiple lines (which is most real JSON), "position 312" tells you almost nothing without doing the counting yourself.

// the exact wording depends on your JS engine, and it's less consistent than you'd expect

These are real, verified outputs from V8 (the engine behind Chrome, Node.js, and Edge) — not paraphrased:

missing comma
JSON.parse('{"a": 1 "b": 2}')
throws
Expected ',' or '}' after property value in JSON at position 8 (line 1 column 9)
empty input
JSON.parse('')
throws
Unexpected end of JSON input
missing value
JSON.parse('{"a":}')
throws
Unexpected token '}', "{"a":}" is not valid JSON
Notice the third example has no position at all. Modern V8 has gotten specific enough to include a line and column directly for some errors (first example) — a genuine improvement over the older, more generic Unexpected token o in JSON at position 5 phrasing that's still what most people picture, and what other engines and older runtimes still produce. Don't assume there's always a number to chase down; sometimes the message is the only information you get.

// why the position points past your mistake, not at it

Take the missing-comma example above: {"a": 1 "b": 2}. The reported position is 8 — the opening quote of "b". But the actual mistake, the missing comma, belongs at position 7, right after the 1. The parser can't report a problem at position 7, because nothing is wrong there yet — a value can legally be followed by whitespace. It's only when the parser reaches the next real token (the " starting "b") and finds something that isn't a comma or a closing brace that it knows something's wrong, and by then it's already one token past where the fix actually belongs.

{"a": 1 "b": 2}comma belongs here (pos 7)error reported here (pos 8)
The fix belongs one character before where the error is actually reported — the parser can only tell something is wrong once it reads far enough to find a token that doesn't fit.

// converting position to line and column yourself

If your engine's error doesn't already include a line and column, you can compute it the same way ToolSharp's JSON Formatter does internally: slice the string up to the reported position, count the newlines in that slice (that's the line number), and the column is the distance from the last newline to the end of the slice.

the whole function
function findLineCol(text, index) {
  const lines = text.slice(0, index).split('\n');
  return { line: lines.length, col: lines[lines.length - 1].length + 1 };
}

// what's usually actually at that position

common root causes
Missing commaerror appears at the start of the next key/value, one token past the actual fix — see above
Trailing commaerror appears at the closing } or ] right after the extra comma
Unquoted or single-quoted keyerror appears at the key itself — JSON requires double-quoted keys, no exceptions
A comment// and /* */ aren't valid JSON at all, despite being legal in JS object literals and JSONC
An invisible characterlooks like nothing's there at that position because, visually, there isn't — see JSON Is Invalid But Looks Correct

// try it yourself

Paste invalid JSON into the JSON Formatter and it converts the raw position into a line and column automatically — no mental arithmetic required. Where the input is repairable, Auto-fix reports exactly what it changed instead of leaving you to guess whether the fix it applied matches what you actually meant.