Fix: Module parse failed: Unexpected token (Webpack / Vite / esbuild)

The Error

You run your build or start your dev server and hit one of these:

Webpack (Create React App, Next.js, or custom config):

Module parse failed: Unexpected token (12:4)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.
Module parse failed: Unexpected token (5:16)
File was processed with these loaders:
 * ./node_modules/babel-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.

Vite (dev server or build):

[vite] Internal server error: Failed to parse source for import analysis because the content contains invalid JS syntax. If you are using JSX, make sure to name the file with the .jsx or .tsx extension.

esbuild:

ERROR: Unexpected ">"
   src/App.js:8:12:
     8 │     return <div className="app">
       ╵             ^
ERROR: The JSX syntax extension is not currently enabled

All of these point to the same root problem: your bundler tried to parse a file but encountered syntax it does not understand. The file contains syntax (JSX, TypeScript, optional chaining, CSS, or something else entirely) that requires a loader, plugin, or transform to convert it into plain JavaScript before the bundler can process it.

Why This Happens

Bundlers do not understand every syntax out of the box. They parse files as plain JavaScript by default. When a file contains syntax that falls outside standard JavaScript — such as JSX angle brackets, TypeScript type annotations, CSS rules, or even newer ECMAScript proposals — the parser chokes on the first token it cannot recognize.

To handle non-standard syntax, bundlers rely on loaders (Webpack), plugins (Vite), or loader configuration (esbuild) to transform source code before parsing. Each file type needs a matching transform:

  • JSX/TSX needs Babel, SWC, or esbuild to compile angle bracket syntax into React.createElement calls (or the JSX runtime equivalent).
  • TypeScript needs ts-loader, babel-loader with @babel/preset-typescript, or esbuild’s built-in TypeScript support to strip type annotations.
  • CSS/SCSS/Less needs css-loader, style-loader, or PostCSS to be handled as non-JavaScript assets.
  • JSON is usually handled natively, but misconfiguration can break it.
  • Images, SVGs, fonts need asset loaders or type declarations.

When the appropriate loader is missing, misconfigured, or applied to the wrong file pattern, the bundler falls back to raw JavaScript parsing and fails at the first unfamiliar token.

Fix 1: Add a Loader for JSX / TSX Files

This is the single most common cause. You have a .js or .jsx file containing JSX syntax, but no Babel or SWC loader is configured to transform it.

Webpack with Babel

Install the required packages:

npm install --save-dev babel-loader @babel/core @babel/preset-env @babel/preset-react

Add the loader rule to your Webpack config:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react'],
          },
        },
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
};

Webpack with SWC

SWC is significantly faster than Babel. Install swc-loader:

npm install --save-dev swc-loader @swc/core
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'swc-loader',
          options: {
            jsc: {
              parser: {
                syntax: 'ecmascript',
                jsx: true,
              },
              transform: {
                react: {
                  runtime: 'automatic',
                },
              },
            },
          },
        },
      },
    ],
  },
};

Vite

Vite uses esbuild internally and handles JSX automatically — but only for files with .jsx or .tsx extensions. If your JSX lives in .js files, rename them to .jsx, or configure Vite’s esbuild options:

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  esbuild: {
    loader: 'jsx',
    include: /\.js$/,
  },
});

If you also need to handle .js files that do not contain JSX, use the esbuild.include pattern to target only the relevant files or directories.

Fix 2: Configure TypeScript Support

TypeScript files contain type annotations (interface, type, generics like <T>) that are not valid JavaScript. Without a TypeScript-aware loader, the parser fails on the first type annotation.

Webpack with ts-loader

npm install --save-dev ts-loader typescript
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: 'ts-loader',
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },
};

Webpack with Babel

If you prefer Babel for TypeScript (faster, but no type checking during build):

npm install --save-dev @babel/preset-typescript

Add the preset to your Babel config:

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript"
  ]
}

And update the Webpack rule to match .ts and .tsx files:

{
  test: /\.(js|jsx|ts|tsx)$/,
  exclude: /node_modules/,
  use: 'babel-loader',
}

Make sure your tsconfig.json exists and has valid configuration. A minimal setup for a React project:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

If TypeScript type errors are also showing up in your build, see Fix: Type ‘X’ is not assignable to type ‘Y’ in TypeScript for common type-level fixes.

Fix 3: Add CSS / SCSS / Less Loaders

Importing a CSS file directly into JavaScript (import './styles.css') fails with “Unexpected token” in Webpack if the CSS loaders are not installed.

npm install --save-dev css-loader style-loader
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

For SCSS:

npm install --save-dev sass-loader sass css-loader style-loader
{
  test: /\.scss$/,
  use: ['style-loader', 'css-loader', 'sass-loader'],
}

For Less:

npm install --save-dev less-loader less css-loader style-loader
{
  test: /\.less$/,
  use: ['style-loader', 'css-loader', 'less-loader'],
}

Important: The order of loaders in the use array matters. Webpack applies them right to left. So sass-loader runs first (compiling SCSS to CSS), then css-loader (resolving @import and url()), then style-loader (injecting CSS into the DOM).

Vite handles CSS, SCSS, and Less natively. If you see a parse error for CSS in Vite, the problem is usually that the file is being treated as JavaScript — check the file extension and import path.

Fix 4: Fix JSON Import Issues

Webpack 5 handles JSON imports natively. However, if you have a custom rule that matches .json files or a misconfigured type field, JSON parsing can break:

// Wrong -- this treats JSON as JavaScript
{
  test: /\.json$/,
  type: 'javascript/auto',
  use: 'some-loader',
}

Remove any custom JSON rules unless you have a specific need. If you do need to customize JSON handling, use type: 'json':

{
  test: /\.json$/,
  type: 'json',
}

If you are importing JSON from an API endpoint or a generated file that has a non-standard extension (like .geojson), add it to the JSON rule:

{
  test: /\.(json|geojson)$/,
  type: 'json',
}

In esbuild, JSON is supported by default. If it is failing, make sure the file is valid JSON — a trailing comma or a comment in the JSON file will cause a parse failure.

Fix 5: Update Parser for Optional Chaining and Nullish Coalescing

If you see “Unexpected token” on a line containing ?. (optional chaining) or ?? (nullish coalescing), your parser or bundler target is too old. These operators are part of ES2020 and are supported natively in all modern environments, but older Babel or Webpack configurations might not handle them.

Babel

Make sure @babel/preset-env is up to date:

npm install --save-dev @babel/core@latest @babel/preset-env@latest

If you are pinned to an older version, you can add the specific plugins:

npm install --save-dev @babel/plugin-proposal-optional-chaining @babel/plugin-proposal-nullish-coalescing-operator
{
  "plugins": [
    "@babel/plugin-proposal-optional-chaining",
    "@babel/plugin-proposal-nullish-coalescing-operator"
  ]
}

Webpack’s acorn parser

If the error comes from Webpack’s own parser (not from a loader), make sure your Webpack version is at least 5.x. Webpack 4’s parser does not support ES2020 syntax natively; it relies on Babel to downlevel the code first. Upgrade to Webpack 5 or ensure all .js files pass through babel-loader before Webpack tries to parse them.

Fix 6: Handle Non-JS File Imports (Images, SVG, Fonts)

Importing images or fonts directly causes a parse failure if there is no matching loader:

import logo from './logo.svg';
import heroImage from './hero.png';

Webpack 5

Use asset modules (no additional packages needed):

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif|webp)$/,
        type: 'asset/resource',
      },
      {
        test: /\.svg$/,
        type: 'asset/resource',
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        type: 'asset/resource',
      },
    ],
  },
};

If you want to use SVGs as React components, install @svgr/webpack:

npm install --save-dev @svgr/webpack
{
  test: /\.svg$/,
  use: ['@svgr/webpack'],
}

esbuild

esbuild needs explicit loader mappings for non-JS file types:

// esbuild.config.js
require('esbuild').build({
  entryPoints: ['src/index.tsx'],
  bundle: true,
  loader: {
    '.png': 'file',
    '.jpg': 'file',
    '.svg': 'file',
    '.woff': 'file',
    '.woff2': 'file',
  },
  outdir: 'dist',
});

If you run into related issues where the build completes but the application fails to load assets at runtime, see Fix: Loading chunk failed for debugging chunk and asset loading errors.

Fix 7: Transpile Dependencies in node_modules

By default, Webpack’s babel-loader excludes node_modules. This works for most packages because they ship pre-compiled JavaScript. But some packages ship untranspiled source code (ESM-only packages, packages with JSX, or packages targeting modern syntax).

When Webpack tries to parse these packages without a loader, you get “Module parse failed.”

Webpack

Modify the exclude pattern to allow specific packages through:

{
  test: /\.(js|jsx)$/,
  exclude: /node_modules\/(?!(some-esm-package|another-package)\/).*/,
  use: 'babel-loader',
}

Or switch from exclude to include:

{
  test: /\.(js|jsx)$/,
  include: [
    path.resolve(__dirname, 'src'),
    path.resolve(__dirname, 'node_modules/some-esm-package'),
  ],
  use: 'babel-loader',
}

Next.js

Next.js provides transpilePackages in next.config.js:

// next.config.js
module.exports = {
  transpilePackages: ['some-esm-package', 'another-package'],
};

This is the cleanest solution if you are using Next.js. It ensures the specified packages pass through the same SWC/Babel pipeline as your own source code.

If the build fails entirely with an exit code error, see Fix: npm ERR! code ELIFECYCLE for broader build failure debugging.

Fix 8: Add vue-loader for Vue Single File Components

Vue .vue files contain <template>, <script>, and <style> blocks in a single file. Without vue-loader, Webpack treats the file as JavaScript and fails on the <template> tag.

npm install --save-dev vue-loader vue-template-compiler
// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      },
      {
        test: /\.css$/,
        use: ['vue-style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new VueLoaderPlugin(),
  ],
};

For Vue 3, use vue-loader v17+ and @vue/compiler-sfc instead of vue-template-compiler:

npm install --save-dev vue-loader@next @vue/compiler-sfc

Vite handles .vue files through @vitejs/plugin-vue:

npm install --save-dev @vitejs/plugin-vue
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
});

Fix 9: Add @babel/preset-env for Modern JavaScript Syntax

If you have babel-loader installed but are missing @babel/preset-env, Babel will run but won’t actually transform modern syntax. The result: Babel outputs the same modern syntax it received, and Webpack’s parser may still choke on it (especially in Webpack 4).

Make sure your Babel config includes the preset:

{
  "presets": ["@babel/preset-env"]
}

Check all places where Babel config can live — any of these can override or shadow each other:

  • babel.config.js or babel.config.json (project-wide)
  • .babelrc or .babelrc.json (directory-specific)
  • "babel" key in package.json
  • Inline options in babel-loader’s Webpack config

If you have multiple config files, Babel’s configuration merging can produce unexpected results. Consolidate into a single babel.config.js at the project root when possible.

Fix 10: Configure Vite’s esbuild Target

Vite uses esbuild for development and Rollup for production builds. If your code or a dependency uses syntax that esbuild does not support at the configured target, you will see parse errors.

Set the esbuild target

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  esbuild: {
    target: 'es2020',
  },
  build: {
    target: 'es2020',
  },
});

Common target values: es2015, es2017, es2020, es2022, esnext. Use esnext during development for maximum speed (no downleveling), and a specific target for production to match your browser support requirements.

JSX in .js files

Vite enforces that JSX syntax must live in .jsx or .tsx files. If you have a .js file containing JSX (common in older React projects), you have two options:

  1. Rename the files from .js to .jsx. This is the recommended approach.
  2. Configure esbuild to treat .js files as JSX (shown in Fix 1 above).

esbuild does not support certain proposals

esbuild does not support every TC39 proposal. Decorators (legacy or stage 3), for example, require a plugin or a different build path. If esbuild fails on decorator syntax, use the Vite plugin @vitejs/plugin-react with SWC or Babel, which can handle decorators:

npm install --save-dev @vitejs/plugin-react
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [['@babel/plugin-proposal-decorators', { version: '2023-11' }]],
      },
    }),
  ],
});

Fix 11: Configure esbuild Loader Mapping

When using esbuild directly (not through Vite), you must explicitly map file extensions to loaders:

require('esbuild').build({
  entryPoints: ['src/index.tsx'],
  bundle: true,
  outdir: 'dist',
  loader: {
    '.ts': 'ts',
    '.tsx': 'tsx',
    '.js': 'js',
    '.jsx': 'jsx',
    '.css': 'css',
    '.json': 'json',
    '.png': 'file',
    '.svg': 'file',
  },
});

If you are getting “Unexpected token” for a specific file type, check whether the corresponding loader is listed. The available esbuild loaders are: js, jsx, ts, tsx, css, json, text, binary, file, dataurl, base64, copy, and default.

For TypeScript files that also contain JSX, use the tsx loader, not ts. The ts loader does not enable JSX parsing:

loader: {
  '.tsx': 'tsx',  // TypeScript + JSX
  '.ts': 'ts',    // TypeScript only
}

If your project uses .ts files that contain JSX (a non-standard pattern), force them through the tsx loader:

loader: {
  '.ts': 'tsx',
}

Still Not Working?

Check the exact file and line number

The error message includes the file path and line number where parsing failed. Open that file and look at the exact line. Common patterns:

  • Angle bracket < — JSX without the right loader.
  • Colon after a variable name (x: string) — TypeScript without the right loader.
  • At sign @ — Decorator syntax without a decorator plugin.
  • Hash # — Private class fields with an outdated parser.
  • CSS selectors (.class, @media) — CSS file being parsed as JS.

Verify which loader actually runs

In Webpack, the error message tells you which loaders processed the file. If you see “File was processed with these loaders” followed by a list, the issue is not that loaders are missing but that the loaders that ran did not fully transform the syntax. You may need an additional loader or a missing Babel preset.

Loader ordering matters

In Webpack, loaders in the use array run from right to left (bottom to top). If you have both babel-loader and ts-loader, ts-loader should run first (rightmost) to strip TypeScript, then babel-loader processes the resulting JavaScript:

{
  test: /\.tsx?$/,
  use: ['babel-loader', 'ts-loader'],  // ts-loader runs first
}

Conflicting rules matching the same file

If two Webpack rules match the same file extension, both apply. This can cause unexpected behavior if one rule handles the file correctly but another interferes. Check all rules in your config and in any merged configs (like from webpack-merge).

The file is actually invalid

Sometimes the file genuinely contains a syntax error — an unclosed bracket, a stray character from a bad merge, or corrupted content. Open the file at the reported line and verify the syntax is correct. Running a standalone parser or linting the file can help. If you encounter issues where the import itself resolves but the module fails to load in the browser, see Fix: Error Cannot find module in Node.js for server-side variants of this problem.

node_modules packages shipping TypeScript or JSX source

Some newer packages ship .ts or .tsx source files without pre-compiling them. This is becoming more common in the ecosystem. Check the package’s main, module, or exports field in its package.json to see what file it points to. If it points to a .ts file, you need to include that package in your loader’s processing scope (see Fix 7).

Webpack version mismatch with loaders

Make sure your loader versions are compatible with your Webpack version. babel-loader 9.x requires Webpack 5. css-loader 7.x requires Webpack 5. Using Webpack 4 with loaders that target Webpack 5 can cause cryptic parse errors.

Check compatibility:

npm ls webpack babel-loader css-loader ts-loader

If you see peer dependency warnings, align the versions. For broader dependency resolution issues, see Fix: Module not found: Can’t resolve which covers module resolution in depth.


Related:

Related Articles