Unexpected Token in JSON at Position 0 and What It Means
An unexpected token error at position 0 means the text is not JSON: usually an HTML error page, the word undefined or an unparsed object. How to tell which.

You call JSON.parse or response.json() and the console says Unexpected token '<', or in an older runtime, Unexpected token < in JSON at position 0. The message is among the most-searched errors in web development, because it appears in the first hour of nearly every project that calls an API. It also has a satisfying property: the position is 0, the very start of the text, and that tells you most of what you need. The parser did not find a broken comma deep in a document. It looked at the first character and could not begin. The input is not JSON, and the question is what it is instead. This article lists the usual answers, shows the messages Node 24 and Python give for each and gives the checks that find the cause in a minute. It is a specific case of the method in fixing JSON parse errors by reading the message properly.
What the message says
JSON values start with one of a small set of characters: {, [, ", a digit or a minus sign, and the first letters of true, false and null. If the first non-whitespace character is anything else, the parser fails there. "Position 0" is that first character, counting from zero. Newer V8 messages show the character and the start of the text, which is more helpful than the old form:
HTML error page Unexpected token '<', "<!DOCTYPE "... is not valid JSON
plain text body Unexpected token 'I', "Internal S"... is not valid JSON
undefined variable "undefined" is not valid JSON
object not stringified "[object Object]" is not valid JSONSo the message itself shows the text that was passed. Read it: "<!DOCTYPE "... is an HTML document, "Internal S"... is the start of Internal Server Error, "undefined" is the string undefined and "[object Object]" is what JavaScript produces when it converts an object to text. In older engines you get only the token and the position, Unexpected token I in JSON at position 0, and have to print the input yourself to see the rest. Python's message is plainer and says only Expecting value: line 1 column 1 (char 0), with no hint of what it saw, so log the text.
The usual causes
An HTML page where JSON was expected. The commonest cause by far. A request failed, and the server, a proxy or a CDN returned an error page, a login page or a 404 document. The first character is <. The URL may be wrong, the route may not exist, the user may be signed out and redirected to a login page, the gateway may have timed out with a 502 or 504. The JSON parser is innocent. Check the status code and the content type of the response; a text/html content type with a 4xx or 5xx status was never going to parse.
A plain-text error body. Some servers answer errors with a sentence, Internal Server Error, Not Found, Forbidden, Too Many Requests. The first character is a letter. The same remedies apply: look at the status, then at what the server sent, then at why. Rate limiting is a surprising source: an API that returns Too Many Requests as text when you call it in a loop turns a working script into a parse error.
**The word undefined or null as text.** A variable that was never set, or the result of a failed lookup, passed straight to JSON.parse. JSON.parse(undefined) converts undefined to the string "undefined" and fails with "undefined" is not valid JSON. Note the odd corner: JSON.parse(null) succeeded and returned null, because null converts to the string "null", which is valid JSON, so a missing value that is null hides the problem while undefined exposes it. A typical bug is reading a value from localStorage or an environment variable that does not exist.
An object that was never stringified. JSON.parse takes a string. If you pass an object, JavaScript converts it to a string first, and the string for any plain object is [object Object]: JSON.parse({ a: 1 }) failed with "[object Object]" is not valid JSON. This usually means the data was already parsed once, by a library that returned an object (axios and fetch(...).json() do), and the code calls JSON.parse on the result again. An array behaves differently: JSON.parse([1, 2]) converts to "1,2", parses the 1 and then fails on the leftover ,2 with Unexpected non-whitespace character after JSON at position 1, an error at position 1, not 0, that confuses people for the same reason.
A byte order mark. The first character is invisible, U+FEFF. The message shows it as Unexpected token '' with an apparently empty token. A file saved by a Windows editor, a response from a server that adds a BOM. Strip it: text.replace(/^/, "") in JavaScript, utf-8-sig in Python. Python on bytes accepts the BOM and Python on text rejects it.
An empty body. Strictly, the error for an empty string is not an unexpected token. Node says Unexpected end of JSON input, since there is no first character, and Python says Expecting value at char 0. People still call it the same family of error: unexpected end of JSON input covers it.
Single-quoted or unquoted text. 'hello' fails with Unexpected token ''' at position 0, because a JSON string needs double quotes, and a bare hello fails at h. See single quotes instead of double quotes.
Find which one
Print the text that failed, and look at the first characters. One line of code does it, and it should be in every catch block:
try {
data = JSON.parse(text);
} catch (e) {
console.error("JSON.parse failed:", e.message, "| first 200 chars:", JSON.stringify(String(text).slice(0, 200)), "| type:", typeof text);
throw e;
}JSON.stringify around the slice shows invisible characters as escapes ("{..."), and typeof text catches the [object Object] case immediately, since it says object where it should say string. For HTTP, add the status and the content type. With fetch, the check that prevents the whole class is short:
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
const data = await res.json();That reads the body as text for the error, which is the only way to see it, and calls .json() only for a success status. You can also check res.headers.get("content-type") for application/json, since some servers return 200 with an HTML body.
From the command line, curl -i URL shows the status line, the headers and the beginning of the body together, and curl -s URL | head -c 200 shows just the start of the body. If curl returns what you expect and your code does not, the difference is in the request: a header, an authentication token or a cookie that the server uses to decide between JSON and a login page. Compare the two requests, starting with Accept and Authorization.
Fix the cause, not the parse
There is nothing to fix in the JSON, because there is none. The repair is wherever the non-JSON came from. For a wrong URL, correct it. For a login redirect, handle the unauthenticated case before parsing, with a status check that routes to sign-in. For a rate limit, back off and retry. For undefined, find where the value should have been set and give it a default or an error. For [object Object], remove the second JSON.parse, or the first conversion. For a BOM, strip it where you read the file or ask for it to be removed at the source. For a proxy error page, add monitoring on the gateway so that you hear about it before your users do, and make the client show "the service is unavailable" and not a syntax error.
Resist the urge to wrap the parse in a try/catch that returns an empty object and carries on. It converts a loud failure into a quiet wrong answer, and the next error appears three screens later, with less information. Catch it where you can say something useful to the user and to the log.
When position 0 is not the real problem
Sometimes position 0 is a symptom of an earlier mistake. A request that goes to the wrong environment returns a valid HTML page from a different application. A cached error response is served for a URL that works now. A service worker returns a stale HTML shell for an API route (a classic in single-page apps: the fallback to index.html answers every unknown path, including API paths, with HTML and a 200 status). A CORS or mixed-content problem returns an opaque response. In each, the text is real and wrong, and the clue is in the headers, the URL and the environment, not in the body.
And when the first character is { or [ and the error is later, you are no longer in this article: read the message for the position and the expectation (trailing commas in JSON and fixing JSON parse errors by reading the message properly cover those). The parser behavior is described in MDN's JSON.parse reference, and Python's in the json module documentation.


