"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.
// what "position" actually counts
// 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:
JSON.parse('{"a": 1 "b": 2}')Expected ',' or '}' after property value in JSON at position 8 (line 1 column 9)
JSON.parse('')Unexpected end of JSON input
JSON.parse('{"a":}')Unexpected token '}', "{"a":}" is not valid JSONUnexpected 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.
// 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.
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
} or ] right after the extra comma// and /* */ aren't valid JSON at all, despite being legal in JS object literals and JSONC// 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.
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.