Fix: ESLint Parsing error: Unexpected token (JSX, TypeScript, ES modules)

The Error

You run ESLint on your project and it throws:

Parsing error: Unexpected token

Or one of its common variants:

Parsing error: Unexpected token <
Parsing error: Unexpected token =>
Parsing error: Unexpected token {
Parsing error: Unexpected token import
Parsing error: Unexpected token :
Parsing error: The keyword 'import' is reserved
Parsing error: Cannot use import statement outside a module

The 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-level await, or class fields may not parse.
  • ES module syntaximport and export statements require sourceType: "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:

SyntaxRequired Setting
import / exportsourceType: "module"
Optional chaining (?.)ecmaVersion: 2020 or later
Nullish coalescing (??)ecmaVersion: 2020 or later
Class fields (x = 1)ecmaVersion: 2022 or later
Top-level awaitecmaVersion: 2022 + sourceType: "module"
import.metaecmaVersion: 2020 + sourceType: "module"
TypeScript syntax@typescript-eslint/parser
JSXecmaFeatures: { 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-plugin

Then 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:

ecmaVersionSyntax 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 import
Parsing error: The keyword 'import' is reserved

ESLint 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/core

Configure 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

SettingLegacy (.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:

  • parser moves inside languageOptions.parser and takes the imported module object, not a string name.
  • parserOptions moves inside languageOptions.parserOptions.
  • plugins is an object mapping names to plugin modules, not an array of strings.
  • extends doesn’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 --version

If 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-vue

Legacy 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/parser as 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-parser

Legacy 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-plugin

Compatible 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 compatible

Fix mismatches by installing matching versions:

npm install --save-dev @typescript-eslint/parser@latest @typescript-eslint/eslint-plugin@latest

If 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.tsx

This 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 eslint

If you see multiple versions, deduplicate:

npm dedupe

Or 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/eslint

Then 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:

  1. Open the command palette (Ctrl+Shift+P / Cmd+Shift+P).
  2. 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 -30

Look for lines like:

eslint:eslint Using config file: /path/to/eslint.config.js
eslint:linter Parsing: /path/to/src/App.tsx with parser espree

If 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