What you'll learn
Quick Answer
SyntaxError: Unexpected token means the JavaScript or JSON parser hit something that cannot legally follow what came before. The reported line is where the parser gave up, so the real mistake is at or before it, never after. The four usual causes are an unbalanced bracket, JSON.parse running on an HTML error page, a trailing comma or comment inside JSON, and mixing import syntax into a CommonJS file. Run node --check to confirm a file parses.
Why the reported line is usually wrong
A parser reads your file from top to bottom, keeping a mental stack of what is still open: this brace belongs to a function, that bracket belongs to an array. It reports an error at the first token that cannot possibly continue a valid program given everything before it. That token is often nowhere near your actual typo.
A missing closing brace is the clearest example. Nothing is wrong at the moment you forget it, because more code could still legally follow. The parser only runs out of options at the end of the file:
function totalFee(items) {
let sum = 0;
for (const item of items) {
sum += item.amount;
return sum;
}
// SyntaxError: Unexpected end of inputHere the final brace closes the for loop, the function is never closed, and the error lands on the last line of a file whose mistake is in the middle. An extra closing brace produces the mirror image: the parser closes a block early, then chokes on the next perfectly innocent line.
The practical rule that follows is worth more than any specific fix. Start at the reported line and read upwards. Look for the nearest opening brace, bracket or parenthesis that has no partner, and for a statement that was cut off halfway. Modern editors will draw a line between matching braces when you click one, so put your cursor on the closing brace of the function that contains the error and see where its partner actually lands. If it lands somewhere you did not expect, you have found the bug.
The related message Unexpected end of input always means something opened and never closed. Unexpected token '}' usually means something closed twice.
When the unexpected token is a less-than sign
This version of the error turns up constantly in real projects, and it has nothing to do with your JavaScript:
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSONOlder engines phrased it as Unexpected token < in JSON at position 0, which hid the clue. The newer message spells it out: the parser was handed a document starting with <!DOCTYPE. Your fetch call asked for JSON and received an HTML page. That happens when the URL is wrong and the server returned its own 404 page, when the request hit a dev server that falls back to index.html for unknown paths, when a session expired and you were redirected to a login page, or when the backend threw and the framework rendered an HTML error page.
The fix is to stop trusting the response and check it before parsing:
const res = await fetch('/api/courses');
if (!res.ok) {
const body = await res.text();
throw new Error(`Request failed ${res.status}: ${body.slice(0, 200)}`);
}
const type = res.headers.get('content-type') || '';
if (!type.includes('application/json')) {
throw new Error(`Expected JSON, got ${type}`);
}
const data = await res.json();Two siblings of this error are worth recognising. SyntaxError: "[object Object]" is not valid JSON means you passed an object to JSON.parse; it was converted to the string [object Object] first, and because that string is short the engine quotes all of it instead of naming a token. Older engines phrased the same failure as Unexpected token o in JSON at position 1. You almost certainly wanted JSON.stringify, or the value was already parsed. And Unexpected end of JSON input means the body was empty, which is exactly what a 204 No Content response or a failed request with no body gives you.
JSON is stricter than JavaScript
JSON looks like a JavaScript object literal, so people copy one into a .json file and are surprised when it breaks. JSON forbids several things that JavaScript happily allows: trailing commas, single quotes, unquoted keys, comments, and undefined as a value. Every one of those is legal in a .js file.
{
"name": "priodemy-api",
"version": "1.0.0",
// this comment is not allowed in JSON
"scripts": {
"dev": "node server.js",
}
}That snippet has two errors: the comment, and the trailing comma after the dev script. If this were package.json, npm would refuse to run anything and report a parse failure that mentions a position number rather than a line.
What trains the bad habit is that tsconfig.json and VS Code's own settings files are JSON with Comments, a relaxed dialect that editors accept. Comments work there and nowhere else, so the muscle memory carries over into files that reject them.
To check a JSON file quickly without installing anything:
node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" package.jsonSilence means the file is valid; anything else prints the failing position. One more cause catches people who copy code from a blog post, a PDF or a WhatsApp message: curly quotation marks. Word processors and messaging apps replace straight quotes with typographic ones, and JavaScript rejects them with SyntaxError: Invalid or unexpected token rather than a helpful message. The same applies to non-breaking spaces and zero-width characters, which are invisible in most editors. If a line looks perfect and still fails, retype it by hand instead of staring at it.
Import, export and the module system mismatch
Node supports two module systems, and mixing them produces syntax errors that describe the symptom rather than the cause. SyntaxError: Cannot use import statement outside a module means a file containing import was parsed as CommonJS. SyntaxError: Unexpected token 'export' is the same problem from the other direction: something loaded an ES module as if it were a script.
Node decides which parser to use from the file extension and the nearest package.json. A .js file is CommonJS unless the package declares otherwise, .mjs is always an ES module and .cjs is always CommonJS. So the fix is one of three things: add "type": "module" to package.json, rename the file to .mjs, or convert the file to require syntax.
Switching to "type": "module" is not free, and this is the part that surprises people. In an ES module require, __dirname and __filename do not exist, and relative imports must include the file extension. Rebuild the missing globals like this:
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);If you only need one ES-only package inside an otherwise CommonJS project, you do not have to migrate everything. A dynamic import works from CommonJS, but a CommonJS file has no top-level await, so the call has to sit inside an async function:
async function main() {
const { default: fetch } = await import('node-fetch');
const res = await fetch('https://example.com');
console.log(res.status);
}
main();Older Node versions throw ERR_REQUIRE_ESM when you require an ES module. Newer versions can load some ES modules through require, but the behaviour depends on the version you are running, so do not build a project around it.
A repeatable way to find the real line
Guessing is slow. Work through this order instead. First, ask Node to parse the file without running it:
node --check src/server.jsThis only reports syntax problems, so it separates a parse failure from a runtime failure in one second. Second, run a formatter. Prettier refuses to format a file it cannot parse and tells you exactly which token defeated it, which is often clearer than the engine's own message. An ESLint run gives you the same information plus the surrounding context.
Third, check which file the error names. A build tool reports errors from the file it was processing, which may be a generated bundle, a dependency, or a template, not the file you had open. If the path contains node_modules or dist, the mistake is upstream of the file you are staring at.
Fourth, use version control. A syntax error appeared because something changed. git diff shows what you touched, and stashing your work confirms whether the last edit caused it. On a large refactor, comment out half the new code, test, then half of what remains. Two or three rounds narrows any file down.
Finally, learn the shape of two specific messages. Unexpected token '<' inside a .js file usually means JSX written in a file the build treats as plain JavaScript, or a <script src> tag whose path is wrong so the browser received index.html and tried to execute it. Unexpected identifier almost always means a missing comma, a missing operator or a missing semicolon between two things that cannot sit next to each other. Once you can map the message to a family of causes, the search stops being random.
