Fix: ESLint Parsing error: Unexpected token (JSX, TypeScript, ES modules)
The Error
You run ESLint on your project and it throws:
Parsing error: Unexpected tokenOr one of its common variants:
Parsing error: Unexpected token <Parsing error: Unexpected token =>Parsing error: Unexpected token {Parsing error: Unexpected token importParsing error: Unexpected token :Parsing error: The keyword 'import' is reservedParsing error: Cannot use import statement outside a moduleThe error often points to a specific line in your code — a JSX angle bracket, a TypeScript type annotation, an arrow function, an import statement, or optional chaining syntax. ESLint stops dead and refuses to lint anything in that file.
This is the most common ESLint configuration error. It means ESLint’s parser doesn’t understand the syntax you’re using. The default parser only handles standard ECMAScript at whatever version you’ve configured. Anything beyond that — JSX, TypeScript, modern ECMAScript features, ES modules — requires explicit configuration.
Why This Happens
ESLint ships with Espree as its default parser. Espree supports standard ECMAScript syntax, but only the version and features you tell it about. Out of the box, it does not understand:
- JSX — the
<Component />syntax used by React and other frameworks. - TypeScript — type annotations like
const x: string, interfaces, generics, enums, and any TS-specific keyword. - Newer ECMAScript features — depending on the configured
ecmaVersion, things like optional chaining (?.), nullish coalescing (??), top-levelawait, or class fields may not parse. - ES module syntax —
importandexportstatements requiresourceType: "module".
When Espree hits syntax it doesn’t recognize, it throws “Parsing error: Unexpected token” and stops. The fix is always one of: switch to a parser that supports your syntax, or tell Espree which syntax features to enable.
Here is a quick reference for which syntax requires which setting:
| Syntax | Required Setting |
|---|---|
import / export | sourceType: "module" |
Optional chaining (?.) | ecmaVersion: 2020 or later |
Nullish coalescing (??) | ecmaVersion: 2020 or later |
Class fields (x = 1) | ecmaVersion: 2022 or later |
Top-level await | ecmaVersion: 2022 + sourceType: "module" |
import.meta | ecmaVersion: 2020 + sourceType: "module" |
| TypeScript syntax | @typescript-eslint/parser |
| JSX | ecmaFeatures: { jsx: true } or a JSX-aware parser |
Decorators (@decorator) | @babel/eslint-parser or @typescript-eslint/parser |
The exact fix depends on what syntax triggered the error. Below are the most common scenarios and their solutions.
Fix 1: Set the Parser to @typescript-eslint/parser (TypeScript Projects)
If you’re writing TypeScript and see the error on a type annotation, interface, enum, or any TypeScript-specific syntax, ESLint needs the TypeScript parser.
Install it:
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-pluginThen configure it in .eslintrc.json:
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
]
}Or in .eslintrc.js:
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
};The @typescript-eslint/parser replaces Espree entirely for .ts and .tsx files. It understands all TypeScript syntax and feeds the AST to ESLint in a format it can work with. Without it, ESLint chokes on the very first colon in a type annotation — which is why you see Parsing error: Unexpected token : on something like const name: string = 'hello'.
If you’re also seeing Type ‘X’ is not assignable to type ‘Y’ errors in your TypeScript project, that is a separate compiler error — but getting the parser right is the first step to having ESLint and TypeScript cooperate properly.
Fix 2: Enable JSX in parserOptions
If the error fires on a < character inside a .jsx or .tsx file, ESLint doesn’t know about JSX syntax.
For plain JavaScript with JSX (React without TypeScript):
{
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
}
}For TypeScript with JSX, set both the parser and jsx:
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"jsx": true
},
"plugins": ["@typescript-eslint"]
}If you use plugin:react/recommended, it typically enables JSX parsing automatically. But if you have a custom config without the React plugin’s recommended settings, you need to set ecmaFeatures.jsx manually.
JSX parsing errors are common when a component file triggers Too many re-renders during development and you try to lint it — the parsing error can mask the real runtime issue if your ESLint config is broken. Fix the ESLint config first so you can actually see the lint warnings that might explain the re-render loop.
Fix 3: Set ecmaVersion to Match Your JavaScript Features
ESLint defaults to a conservative ECMAScript version in legacy configs. If you use modern syntax — optional chaining, nullish coalescing, class fields, top-level await — you need to tell ESLint which version to parse.
{
"parserOptions": {
"ecmaVersion": 2022
}
}Common values and what they unlock:
ecmaVersion | Syntax Added |
|---|---|
2015 (6) | let, const, arrow functions, template literals, destructuring, classes, modules |
2017 (8) | async/await |
2020 (11) | Optional chaining (?.), nullish coalescing (??), BigInt, globalThis, import.meta |
2021 (12) | Logical assignment (&&=, ` |
2022 (13) | Top-level await, class fields, private methods, static initialization blocks |
2023 (14) | Hashbang comments (#!/usr/bin/env node) |
"latest" | Always the most recent supported version |
Setting ecmaVersion: "latest" is the safest choice for new projects. It tells Espree to support whatever the current version of ESLint supports.
If you’re on an older ecmaVersion and your code uses something like import() dynamic imports, you’ll get the unexpected token error. This is separate from the Module not found: Can’t resolve error you’d see at build time — that’s a bundler issue, while this is purely a parser issue.
Fix 4: Set sourceType to "module"
If the error fires on an import or export statement:
Parsing error: Unexpected token importParsing error: The keyword 'import' is reservedESLint defaults to sourceType: "script", which doesn’t allow import/export. You need to switch to module mode:
{
"parserOptions": {
"sourceType": "module"
}
}For Node.js projects using CommonJS in some files and ES modules in others, you can use overrides:
{
"parserOptions": {
"sourceType": "script"
},
"overrides": [
{
"files": ["*.mjs", "src/**/*.js"],
"parserOptions": {
"sourceType": "module"
}
}
]
}Note that import.meta requires both sourceType: "module" and ecmaVersion: 2020 or later. Missing either one triggers the error. This catches people who set ecmaVersion: "latest" but forget sourceType: "module".
Fix 5: Use @babel/eslint-parser (Babel Projects)
If you use Babel for transpilation (common in projects not using TypeScript), Babel might support syntax that Espree doesn’t — decorators, pipeline operators, or other stage-3 proposals.
Install the Babel parser for ESLint:
npm install --save-dev @babel/eslint-parser @babel/coreConfigure it:
{
"parser": "@babel/eslint-parser",
"parserOptions": {
"requireConfigFile": false,
"babelOptions": {
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
}
}@babel/eslint-parser (formerly babel-eslint) delegates parsing to Babel, so any syntax your Babel config supports, ESLint will also understand. If you have a .babelrc or babel.config.js, you can omit babelOptions and set requireConfigFile: true (the default).
Note: don’t use both @babel/eslint-parser and @typescript-eslint/parser in the same config scope. Pick one based on your toolchain. TypeScript projects should use @typescript-eslint/parser. If you need both in a mixed project, use overrides to assign different parsers to different file extensions.
If your Babel setup is also causing build failures, you might be dealing with a Webpack Module parse failed: Unexpected token error, which has a similar root cause — the bundler’s loader chain doesn’t understand the syntax either.
Fix 6: eslint.config.js (Flat Config) vs .eslintrc (Legacy Config)
ESLint 9+ uses the flat config format (eslint.config.js) by default, replacing .eslintrc.*. The configuration structure is completely different, and mixing up the two formats causes parsing errors.
Key Structural Differences
| Setting | Legacy (.eslintrc) | Flat (eslint.config.js) |
|---|---|---|
| Parser | "parser": "@typescript-eslint/parser" | languageOptions: { parser: importedParser } |
ecmaVersion | "parserOptions": { "ecmaVersion": "latest" } | languageOptions: { ecmaVersion: "latest" } |
sourceType | "parserOptions": { "sourceType": "module" } | languageOptions: { sourceType: "module" } |
| JSX | "parserOptions": { "ecmaFeatures": { "jsx": true } } | languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } |
| Globals | "env": { "browser": true } | languageOptions: { globals: { ...globals.browser } } |
Flat config format (eslint.config.js):
import typescriptParser from '@typescript-eslint/parser';
import typescriptPlugin from '@typescript-eslint/eslint-plugin';
export default [
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
'@typescript-eslint': typescriptPlugin,
},
rules: {
...typescriptPlugin.configs.recommended.rules,
},
},
];Critical differences from .eslintrc:
parsermoves insidelanguageOptions.parserand takes the imported module object, not a string name.parserOptionsmoves insidelanguageOptions.parserOptions.pluginsis an object mapping names to plugin modules, not an array of strings.extendsdoesn’t exist — you spread rule configs manually or use the plugin’s flat config helpers.
If you copy an .eslintrc config into eslint.config.js without restructuring it, ESLint won’t find the parser and will fall back to Espree, causing unexpected token errors on the first line of TypeScript or JSX.
Also watch for the config file format itself: eslint.config.js uses export default (ES module syntax). If your package.json doesn’t have "type": "module", rename the file to eslint.config.mjs. If your package.json does have "type": "module" and you need a CommonJS config, use eslint.config.cjs.
ESLint 9 Ignores .eslintrc by Default
ESLint 9 only reads eslint.config.js. If you upgraded ESLint to v9 but still have a .eslintrc.json or .eslintrc.js file, ESLint silently ignores it and uses Espree with default settings. All your parser configuration vanishes. Check your ESLint version:
npx eslint --versionIf you’re on v9+ and still need legacy config temporarily, set ESLINT_USE_FLAT_CONFIG=false as an environment variable.
Fix 7: TypeScript + React Project Setup
A complete configuration for a project using TypeScript and React, covering both .ts and .tsx files:
Legacy config (.eslintrc.json):
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint", "react", "react-hooks"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"settings": {
"react": {
"version": "detect"
}
}
}Flat config (eslint.config.js):
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import globals from 'globals';
export default [
...tseslint.configs.recommended,
{
files: ['**/*.{ts,tsx,js,jsx}'],
plugins: {
react,
'react-hooks': reactHooks,
},
languageOptions: {
globals: {
...globals.browser,
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
settings: {
react: {
version: 'detect',
},
},
rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
},
},
];The project: "./tsconfig.json" option in legacy config enables type-aware linting rules. Without it, rules like @typescript-eslint/no-floating-promises won’t work. But adding it can slow down linting significantly on large codebases.
If you set project and then see Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser, make sure the files being linted are actually included in your tsconfig.json. Files outside include or inside exclude will trigger this secondary error. Common offenders are config files in the project root (like jest.config.ts, vite.config.ts) that aren’t covered by your main tsconfig.json. Either add them to include or create a tsconfig.eslint.json that extends your base config with broader includes:
{
"extends": "./tsconfig.json",
"include": ["src", "*.config.ts", "*.config.js"]
}Then point ESLint to this config:
{
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}Fix 8: Vue and Svelte File Parsing
Single-file components in Vue (.vue) and Svelte (.svelte) need dedicated parsers because they embed HTML, CSS, and JavaScript/TypeScript in one file.
Vue
Install the parser and plugin:
npm install --save-dev eslint-plugin-vueLegacy config:
{
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": "latest",
"sourceType": "module"
},
"extends": [
"plugin:vue/vue3-recommended",
"plugin:@typescript-eslint/recommended"
]
}Notice the double parser setup. The top-level parser is vue-eslint-parser, which handles the .vue file structure (template, script, style blocks). The parser inside parserOptions handles the JavaScript/TypeScript within <script> blocks. This is a very common source of confusion:
- If you set
@typescript-eslint/parseras the top-level parser, Vue template syntax will fail because the TypeScript parser can’t handle<template>blocks. - If you omit the inner parser, TypeScript in
<script lang="ts">blocks will fail because Espree can’t handle type annotations.
You must have both.
Flat config:
import vue from 'eslint-plugin-vue';
import tsParser from '@typescript-eslint/parser';
export default [
...vue.configs['flat/recommended'],
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: tsParser,
},
},
},
];Svelte
Install the parser and plugin:
npm install --save-dev eslint-plugin-svelte svelte-eslint-parserLegacy config:
{
"overrides": [
{
"files": ["*.svelte"],
"parser": "svelte-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser"
}
}
]
}Flat config:
import svelte from 'eslint-plugin-svelte';
export default [
...svelte.configs['flat/recommended'],
];The same nested parser pattern applies as with Vue. The framework-specific parser handles the component structure while delegating script parsing to @typescript-eslint/parser or Espree.
Fix 9: ESLint Plugin Conflicts and Version Mismatches
Sometimes the parser is configured correctly but a plugin conflict causes the parsing error. This is especially common in three scenarios:
Parser and Plugin Versions Out of Sync
@typescript-eslint/parser and @typescript-eslint/eslint-plugin must be on the same major version. Mixing v5 of the plugin with v6 of the parser (or vice versa) causes unpredictable failures, including parsing errors.
Check for version mismatches:
npm ls @typescript-eslint/parser @typescript-eslint/eslint-pluginCompatible combinations:
eslint@8.x + @typescript-eslint/*@5.x ---- compatible
eslint@8.x + @typescript-eslint/*@6.x ---- compatible
eslint@8.x + @typescript-eslint/*@7.x ---- compatible
eslint@9.x + @typescript-eslint/*@8.x ---- compatible
eslint@9.x + @typescript-eslint/*@5.x ---- NOT compatible
eslint@9.x + @typescript-eslint/*@6.x ---- NOT compatibleFix mismatches by installing matching versions:
npm install --save-dev @typescript-eslint/parser@latest @typescript-eslint/eslint-plugin@latestIf the error appeared after running npm install and you suddenly see build failures like npm ERR! code ELIFECYCLE, a dependency update likely pulled in an incompatible ESLint plugin version. Check your package-lock.json for duplicated ESLint-related packages.
Shared Configs Overriding Your Parser
A shared config like eslint-config-airbnb or eslint-config-standard might set its own parser, overriding yours. Debug the effective config with:
npx eslint --print-config src/App.tsxThis prints the fully merged configuration for a specific file, showing you exactly which parser and options are in effect. If the parser listed isn’t the one you configured, a shared config or override is responsible.
Multiple Versions of ESLint Installed
If a dependency installs its own version of ESLint, you can end up with two copies. Plugins loaded by one version may not be available to the other.
npm ls eslintIf you see multiple versions, deduplicate:
npm dedupeOr add an overrides field in package.json to force a single version:
{
"overrides": {
"eslint": "^9.0.0"
}
}Fix 10: Use .eslintignore for Generated Files
Sometimes the parsing error comes from a file you never wrote — generated code, build output, or vendored third-party files. These files often contain syntax that doesn’t match your project’s ESLint configuration.
Common culprits:
dist/,build/,.next/,.nuxt/,out/directories- Auto-generated GraphQL types
- Service worker files generated by Workbox
- Files generated by code generation tools (Prisma client, protobuf, OpenAPI generators)
- CSS-in-JS generated files
- Coverage reports in
coverage/
Create an .eslintignore file (or add to your existing one):
dist/
build/
.next/
.nuxt/
coverage/
node_modules/
*.generated.ts
*.generated.js
src/generated/Or set ignorePatterns in .eslintrc.json:
{
"ignorePatterns": [
"dist/",
"build/",
".next/",
"coverage/",
"*.generated.*",
"src/generated/"
]
}In flat config (eslint.config.js), use the ignores property as a standalone config object:
export default [
{
ignores: [
'dist/**',
'build/**',
'.next/**',
'coverage/**',
'*.generated.*',
'src/generated/**',
],
},
// ... rest of your config
];In flat config, the ignores must be in its own object (without other keys like rules or files) to act as a global ignore. If you put ignores inside a config object that also has rules, it only applies to that specific config block — not globally.
If you’re running ESLint on a project that also has module resolution issues at build time — such as Module not found: Can’t resolve — the generated files that ESLint chokes on may be the same ones your bundler can’t find. Fixing the build pipeline usually fixes the lint errors on those files too.
Still Not Working?
The Error Points to a Perfectly Valid Line
ESLint parsing errors don’t always point to the actual problem. A missing closing brace, parenthesis, or bracket earlier in the file can cause the parser to lose track and report the error much later. If the line ESLint points to looks correct, scroll up and look for:
- Unclosed template literals (backtick strings spanning multiple lines)
- Missing closing braces on objects, functions, or classes
- Unclosed JSX tags or mismatched JSX element names
- A stray
<or>from a copy-paste error - An extra comma after the last property in a JSON-like object (trailing commas in certain contexts)
Run Prettier or your editor’s auto-formatter on the file first. If it also fails, the syntax error is real and not an ESLint config problem.
The Error Only Happens in CI
Your local machine might use a different ESLint version, Node.js version, or have a global ESLint installation that overrides the local one. Always run ESLint through a local npx eslint or a package.json script, never a global install.
Check Node.js version requirements — @typescript-eslint/parser v7+ requires Node.js 18 or later. Running on Node 16 in CI while using Node 20 locally will cause failures that you never see on your machine.
Also check that CI runs npm ci (not npm install) to ensure dependencies match the lockfile exactly. A different resolved version of @typescript-eslint/parser in CI vs. locally can produce different parsing behavior.
--ext Flag Missing (ESLint 8 and Below)
By default, ESLint 8 and below only lints .js files. If you run npx eslint src/, it will skip .ts, .tsx, .jsx, and .vue files entirely — or worse, try to parse them with the wrong settings if a glob pattern pulls them in.
Pass the extensions explicitly:
npx eslint --ext .js,.jsx,.ts,.tsx src/In ESLint 9 with flat config, the files property in each config object controls which files match, so --ext is no longer needed.
Multiple tsconfig.json Files (Monorepos)
In a monorepo with multiple tsconfig.json files, the project option in parserOptions needs to point to the right one for each package. Use an array or a glob:
{
"parserOptions": {
"project": ["./tsconfig.json", "./packages/*/tsconfig.json"]
}
}Or use tsconfigRootDir to set the base directory:
module.exports = {
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
};Without tsconfigRootDir, ESLint resolves the project path relative to the working directory, which may differ from the config file’s directory in monorepo setups.
ESLint Cache Is Stale
ESLint caches results to speed up repeat runs. If you changed your config but ESLint still reports old errors, clear the cache:
rm -f .eslintcache
rm -rf node_modules/.cache/eslintThen re-run ESLint. If you’re using a CI system that caches node_modules, make sure the ESLint cache isn’t persisted between runs with different config files.
VS Code ESLint Extension Not Picking Up Changes
The VS Code ESLint extension caches configuration. After changing your ESLint config, restart the ESLint server:
- Open the command palette (
Ctrl+Shift+P/Cmd+Shift+P). - Run “ESLint: Restart ESLint Server”.
If that doesn’t work, run “Developer: Reload Window” to fully reset the extension.
Check the ESLint output channel for detailed errors: go to View > Output and select ESLint from the dropdown. Parser loading failures and config resolution issues show up here and are often more descriptive than the inline error message in the editor.
Verify Your Config Is Actually Being Loaded
Run ESLint with the --debug flag to see exactly which config file it reads and which parser it uses:
npx eslint --debug src/App.tsx 2>&1 | head -30Look for lines like:
eslint:eslint Using config file: /path/to/eslint.config.js
eslint:linter Parsing: /path/to/src/App.tsx with parser espreeIf it says parser espree when you expected @typescript-eslint/parser, your parser config isn’t being applied to that file. Check the files patterns in your config to ensure they match the file being linted.
Related: Fix: TS2322 Type ‘X’ is not assignable to type ‘Y’ | Fix: Too many re-renders | Fix: Module not found: Can’t resolve | Fix: Webpack Module parse failed: Unexpected token | Fix: npm ERR! code ELIFECYCLE
Related Articles
Fix: VSCode ESLint Extension Not Working – No Errors Shown or Not Running
How to fix VSCode's ESLint extension when it stops highlighting errors, shows no output, or fails to start. Covers flat config, settings, and common misconfigurations.
Fix: Module parse failed: Unexpected token (Webpack / Vite / esbuild)
How to fix 'Module parse failed: Unexpected token' in Webpack, Vite, and esbuild by configuring the correct loaders and transforms for JSX, TypeScript, CSS, JSON, and other file types.
Fix: TS2322 Type 'X' is not assignable to type 'Y'
How to fix TypeScript error TS2322 'Type is not assignable to type'. Covers literal types vs general types, string vs String, union types, interface compatibility, generic constraints, readonly arrays, excess property checking, discriminated unions, type assertions, type widening and narrowing, React event handlers, Promise return types, and enum mismatches.
Fix: SyntaxError: Unexpected token < in JSON at position 0
How to fix 'Unexpected token in JSON at position 0', 'JSON.parse: unexpected character', and 'Unexpected end of JSON input' in JavaScript and TypeScript. Covers API returning HTML instead of JSON, Content-Type mismatches, fetch URL typos, invalid JSON syntax, BOM characters, CORS proxies, and debugging with response.text().