Unexpected token in JavaScript: the two different errors
Unexpected token is two different errors. One is a typo in your file, the other means you got HTML where you expected JSON.
Unexpected token is two unrelated errors wearing the same name. One is a typo in a file you wrote, caught before any of your code runs. The other happens at runtime when JSON.parse is handed something that is not JSON, which is almost always an HTML page.
They have different causes and different fixes, so the first move is to work out which one you have.
Look at the end of the message. If it says is not valid JSON, it is the runtime kind and your code is fine. If instead it points at a file and line with a caret underneath, it is the parse kind and there is a character to fix.
Every message on this page came out of Node v24.14.1.
The parse error: a typo in your file
This version stops the file loading. Nothing in it runs, which is why adding console.log at the top changes nothing.
Node prints the offending line with a caret under the exact character:
const o = {a: 1,, b: 2}
^
SyntaxError: Unexpected token ','
The caret is the useful part. The character it points at is not necessarily wrong, but it is where the parser gave up, and the real mistake is at or just before it.
Common versions, all confirmed:
| Code | Message |
|---|---|
if (x > 1 { |
Unexpected token '{' |
function f() { return 1 }} |
Unexpected token '}' |
const class = 5 |
Unexpected token 'class' |
function f(a, b,,) {} |
Unexpected token ',' |
The third one catches people out. class is a reserved word, and so are const, new, return, default and about thirty others. You cannot use them as variable names, and the message does not explain that, it just reports the word as unexpected.
When a bracket is missing you get a different message
This is worth knowing because it changes where you look:
function greet() {
console.log("hi")
console.log("after")
SyntaxError: Unexpected end of input
Not "unexpected token". When you leave a bracket open, the parser reads to the end of the file still waiting for it, and reports running out of input. So:
Unexpected token '}'means one too many closing brackets.Unexpected end of inputmeans one too few.
That single distinction tells you which direction to search, which matters in a long file. Your editor's bracket matching will find the pair faster than reading will.
The near-miss messages
Three more parse failures produce wording close enough to confuse, and each points somewhere specific:
| Code | Message | What it means |
|---|---|---|
export const a = 1 in a .cjs file |
Unexpected token 'export' |
Module format mismatch |
const s = "unterminated |
Invalid or unexpected token |
A quote is never closed |
{ "a": 1 "b": 2 } |
Unexpected string |
Missing comma between properties |
The first is the one to recognise on sight. Unexpected token 'export' or Unexpected token 'import' almost never means you mistyped the keyword. It means the file is being read as CommonJS while containing ES module syntax. Either rename the file to .mjs, add "type": "module" to package.json, or convert the syntax to require. The mirror image, require inside an ES module, does not produce a syntax error at all: it reports require is not defined in ES module scope.
The other two are useful because the wording tells you the shape of the mistake before you have found it. Invalid or unexpected token points at a string that never ends, and the highlighting in your editor will show it as a long run of string colour. Unexpected string means the parser met a value where it wanted a separator, which is a missing comma nearly every time.
The < that means you got HTML
This is the most common version of the error in real projects, and it has nothing to do with your syntax:
JSON.parse("<!DOCTYPE html>\n<html><body>Cannot GET /api/users</body></html>");
// SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Read the message closely, because it is more helpful than it looks. It quotes the first characters of what it was given. "<!DOCTYPE " is the beginning of an HTML page. Your request returned a web page, and the code tried to parse it as data.
Nearly always, this means the request went somewhere that does not serve JSON:
- The URL is wrong, so the server returned its 404 page. An Express 404 gives
Unexpected token 'C', "Cannot GET"..., which quotes the response body back at you and names the problem outright. - The request hit the frontend dev server instead of the API, so it returned
index.html. This is the classic missing-proxy case. - You are logged out, and the server sent a login page instead of data.
- The server threw, and returned an HTML error page with a 500 status.
The fix is never in the parsing code. Open the Network tab, find the request, and look at the response body and status. What the message quotes is what the server sent.
Two versions of the same < appear in source files rather than responses:
SyntaxError: Unexpected token '<'
A .js file whose contents are actually HTML produces this, usually because a <script src> points at the wrong path and the server returned a page. So does JSX in a file that is not being compiled, since const el = <h1>hi</h1> is not valid JavaScript on its own. All three cases share one lesson: a < where JavaScript expected an expression means something handed you markup.
The JSON errors that are not <
The rest of the JSON.parse failures are usually malformed data rather than the wrong data:
| Input | Message |
|---|---|
"" (empty response) |
Unexpected end of JSON input |
{"a": 1,} |
Expected double-quoted property name in JSON at position 8 |
{'a': 1} |
Expected property name or '}' in JSON at position 1 |
undefined |
"undefined" is not valid JSON |
The empty-response case is worth calling out. A 204 No Content reply has an empty body by design, so calling .json() on it always throws. Check the status before parsing rather than after.
The trailing comma is the one that surprises people who write a lot of JavaScript. {a: 1,} is legal in a .js file and illegal in JSON. JSON also requires double quotes on keys, which is why the single-quoted version fails at position 1.
Note that these messages give a character position rather than a line and column of your source. at position 8 counts into the string being parsed, so logging the raw text and counting to that character finds it.
Preventing both kinds
For the parse error, the tooling does it for you. An editor with syntax highlighting shows an unterminated string as a wall of the wrong colour, and a linter reports the problem as you type rather than at run time. A formatter such as Prettier will refuse to format a file it cannot parse, which is a useful early signal in itself.
For the JSON error, stop trusting that a response is JSON:
const res = await fetch("/api/users");
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const data = await res.json();
Checking res.ok first turns a confusing Unexpected token '<' into a clear Request failed: 404, because fetch does not treat a 404 as a failure. It resolves normally and hands you the error page, which is exactly how the HTML ends up in the parser. Only a network-level failure rejects the promise, so a .catch() on its own will not save you from this.
The habit worth building is reading the whole message rather than the first three words. Unexpected token '<', "<!DOCTYPE "... already tells you the server sent a web page. Most of the time the answer is in the part after the comma.
Want to see these fail in front of you? Start with the JavaScript track and run the broken versions in an editor.
More from the blog
Objects are not valid as a React child: what to render
Objects are not valid as a React child, and React tells you which one by listing its keys. In React 19 the promise version throws a different error.
Read morenpm install errors: read the code, not the stack
Every npm ERR code names one cause. ERESOLVE, E404, EJSONPARSE and the rest, with what each one means, measured against real installs.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.