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 enabledAll 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.createElementcalls (or the JSX runtime equivalent). - TypeScript needs
ts-loader,babel-loaderwith@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-reactAdd 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-typescriptAdd 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@latestIf 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-sfcVite 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.jsorbabel.config.json(project-wide).babelrcor.babelrc.json(directory-specific)"babel"key inpackage.json- Inline
optionsinbabel-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:
- Rename the files from
.jsto.jsx. This is the recommended approach. - Configure esbuild to treat
.jsfiles 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-loaderIf 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:
- Fix: Module not found: Can’t resolve — for module resolution and import path errors.
- Fix: Error Cannot find module in Node.js — for Node.js runtime module loading errors.
- Fix: Type ‘X’ is not assignable to type ‘Y’ in TypeScript — for TypeScript type errors during compilation.
- Fix: npm ERR! code ELIFECYCLE — for build script exit code failures.
- Fix: Loading chunk failed — for runtime chunk loading errors after a successful build.
Related Articles
Fix: Module not found: Can't resolve / Cannot find module or its corresponding type declarations
How to fix 'Module not found: Can't resolve' in webpack, Vite, and React, and 'Cannot find module or its corresponding type declarations' in TypeScript. Covers missing packages, wrong import paths, case sensitivity, path aliases, node_modules corruption, monorepo hoisting, barrel files, and asset imports.
Fix: Webpack Module Not Found – Can't Resolve '<module>' in '<directory>'
How to fix the Webpack error 'Module not found: Error: Can't resolve' caused by missing packages, wrong paths, aliases, or extension resolution issues.
Fix: ESLint Parsing error: Unexpected token (JSX, TypeScript, ES modules)
How to fix ESLint 'Parsing error: Unexpected token' for JSX, TypeScript, and ES module syntax by configuring the correct parser, parserOptions, and ESLint config format.
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.