Fix: SyntaxError: Cannot use import statement outside a module
Quick Answer
How to fix 'SyntaxError: Cannot use import statement outside a module' in Node.js, TypeScript, Jest, and browsers by configuring ESM, package.json type, and transpiler settings.
The Error
You run a JavaScript or TypeScript file and hit this:
SyntaxError: Cannot use import statement outside a module
at wrapSafe (internal/modules/cjs/loader.js:915:16)
at Module._compile (internal/modules/cjs/loader.js:963:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)Or in a browser console:
Uncaught SyntaxError: Cannot use import statement outside a moduleThe line causing it looks perfectly normal:
import express from 'express';Nothing wrong with the syntax. The problem is that the runtime doesn’t know your file is an ES module.
Why This Happens
JavaScript has two module systems: CommonJS (CJS) and ES Modules (ESM). They are fundamentally incompatible in how they load code.
CommonJS uses require() and module.exports:
const express = require('express');
module.exports = { app };ES Modules use import and export:
import express from 'express';
export { app };Here is the key point: Node.js treats .js files as CommonJS by default. When Node encounters an import statement in a file it considers CommonJS, it throws SyntaxError: Cannot use import statement outside a module because import is not valid CJS syntax.
The same applies in browsers. A <script> tag without type="module" runs in classic script mode, where import statements are illegal.
This error shows up in several common scenarios:
- You wrote ESM code but your
package.jsonlacks"type": "module" - Your TypeScript config compiles to ESM but Node expects CJS
- Your test runner (Jest, Mocha, Vitest) doesn’t support ESM imports
- You’re loading a script in the browser without
type="module" - A dependency ships ESM-only code and your project is CJS
- Your Babel or transpiler config isn’t processing
importstatements
Each scenario has a different fix. Work through the ones that match your setup.
Fix 1: Set "type": "module" in package.json
This is the most common fix for Node.js projects. Open your package.json and add the type field:
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"main": "index.js"
}Setting "type": "module" tells Node.js to treat every .js file in your project as an ES module. After this change, import and export statements work without any extra flags.
Warning: This is a project-wide change. Every .js file in your package must now use import/export instead of require()/module.exports. If you have existing CJS files, you need to either convert them or rename them to .cjs (see Fix 2).
After adding "type": "module", you also need to update a few things:
- Use file extensions in imports. ESM in Node.js requires explicit extensions:
// Before (CJS-style, no extension)
import { helper } from './utils/helper';
// After (ESM requires the extension)
import { helper } from './utils/helper.js';- Replace
__dirnameand__filename. These globals don’t exist in ESM:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);- Replace
require()for JSON files. You can’trequire()in ESM:
// Option A: import assertion (Node 17.5+)
import config from './config.json' with { type: 'json' };
// Option B: read and parse manually
import { readFileSync } from 'fs';
const config = JSON.parse(readFileSync('./config.json', 'utf-8'));Pro Tip: If you only want ESM in a few files without converting your entire project, rename those specific files to
.mjs. Node.js always treats.mjsfiles as ES modules regardless of the"type"field inpackage.json. This lets you adopt ESM incrementally.
Fix 2: Rename Files to .mjs or .cjs
Node.js uses file extensions to determine the module type, and extensions override the package.json "type" field:
.mjsfiles are always ES modules.cjsfiles are always CommonJS.jsfiles follow the"type"field (default: CommonJS)
If you want to use import in one specific file without changing your entire project:
mv index.js index.mjs
node index.mjsThis is useful when you have a mixed codebase — some files using require() and others using import. Rename accordingly:
project/
├── package.json # no "type": "module"
├── server.cjs # CommonJS (uses require)
├── utils.mjs # ES Module (uses import)
└── scripts/
└── build.mjs # ES ModuleIf your project has "type": "module" set and you need a file to use require(), rename it to .cjs:
// legacy-config.cjs — this file uses require() even in an ESM project
const dotenv = require('dotenv');
dotenv.config();
module.exports = { /* ... */ };Fix 3: Configure TypeScript Correctly
TypeScript has its own module system settings in tsconfig.json. A misconfigured module or moduleResolution field causes this error when running compiled output.
For Node.js ESM Projects (Node 16+)
{
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16",
"target": "ES2022",
"outDir": "./dist",
"esModuleInterop": true
}
}With this config, TypeScript respects your package.json "type" field. If "type": "module" is set, compiled .js files will use ESM syntax and Node will handle them correctly.
For CJS Output (No “type”: “module”)
If you want TypeScript to compile import statements down to require() calls:
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "ES2020",
"outDir": "./dist",
"esModuleInterop": true
}
}This approach lets you write import in your .ts source files, and TypeScript compiles them to require() in the output. No "type": "module" needed.
Running TypeScript Directly with ts-node
ts-node needs extra configuration for ESM. Add this to your tsconfig.json:
{
"ts-node": {
"esm": true
},
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16"
}
}Then run with:
node --loader ts-node/esm index.tsOr in recent Node versions (v20.6+):
node --import tsx index.tsUsing tsx as an alternative to ts-node avoids many ESM configuration headaches. Install it with npm install -D tsx.
If you’re running into module resolution issues alongside this error, check out how to fix TypeScript cannot find module errors for detailed moduleResolution guidance.
Fix 4: Configure Jest for ES Modules
Jest runs in a CommonJS environment by default. When your test files or source files use import, you get this error immediately.
Option A: Use Babel to Transform Imports
Install the required packages:
npm install -D @babel/core @babel/preset-env babel-jestCreate or update babel.config.js (use .cjs if your project has "type": "module"):
// babel.config.cjs
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
],
};Jest automatically uses babel-jest to transform files. This converts import to require() before running tests.
For TypeScript projects, add @babel/preset-typescript:
npm install -D @babel/preset-typescript// babel.config.cjs
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};Option B: Use Jest’s Experimental ESM Support
Add "type": "module" to your package.json, then run Jest with the --experimental-vm-modules flag:
NODE_OPTIONS='--experimental-vm-modules' npx jestUpdate your jest.config.js to handle ESM:
export default {
transform: {},
};Setting transform: {} tells Jest not to transform your files — it will run them as native ESM instead.
Note: Jest’s ESM support has been experimental for a long time. If you want stability, the Babel approach (Option A) is more reliable. Alternatively, consider switching to Vitest, which supports ESM natively without any configuration.
Option C: Switch to Vitest
If you’re starting a new project or are tired of fighting Jest’s ESM support:
npm install -D vitest// vitest.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
},
});Vitest handles ESM, TypeScript, and JSX out of the box with no extra configuration. It’s a drop-in alternative to Jest with a near-identical API.
Fix 5: Use type="module" in Browser Script Tags
In the browser, a regular <script> tag treats JavaScript as a classic script. import statements are not allowed in classic scripts.
This fails:
<script src="app.js"></script>Add type="module":
<script type="module" src="app.js"></script>There are important differences between module scripts and classic scripts:
- Module scripts are deferred by default. They execute after the HTML is parsed, like adding the
deferattribute. You don’t needDOMContentLoadedlisteners in most cases. - Module scripts use strict mode automatically. No need for
"use strict". - Module scripts have their own scope. Variables declared at the top level are not added to
window. - CORS is enforced. If you load a module from a CDN or another domain, the server must send appropriate CORS headers.
For inline scripts:
<!-- This fails -->
<script>
import { render } from './renderer.js';
</script>
<!-- This works -->
<script type="module">
import { render } from './renderer.js';
</script>Warning: You cannot use bare specifiers in browser ESM. This does not work:
// Fails in browsers — no bare specifier resolution
import React from 'react';You need a full path or a bundler (Vite, Webpack, etc.) to resolve node_modules packages. If you’re seeing module resolution errors alongside this, check out how to fix module not found errors.
Fix 6: Configure Babel or Your Transpiler
If you’re using Babel but import statements aren’t being transformed, your Babel configuration is missing or misconfigured.
Check Your Babel Config
Make sure @babel/preset-env is installed and configured:
npm install -D @babel/core @babel/preset-envYour Babel config file (.babelrc, babel.config.js, or babel.config.json) should include:
{
"presets": ["@babel/preset-env"]
}Babel Isn’t Running at All
This happens more often than you’d think. Common causes:
- No Babel loader in Webpack. If you’re using Webpack, make sure
babel-loaderis configured:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader',
},
],
},
};If your Webpack build fails with unexpected token errors, see fixing Webpack module parse failures for more solutions.
- Babel config file not being picked up. Babel looks for config files in specific locations. If your config is in a monorepo root but your package is in a subdirectory, Babel might not find it. Use
babel.config.js(not.babelrc) at the monorepo root and setrootMode:
// In your package's local .babelrc
{
"rootMode": "upward"
}- File extension not being processed. If your files use
.mjsor.tsx, make sure your build tool is configured to process those extensions.
Using SWC Instead of Babel
SWC is a faster alternative that handles ESM transformation:
npm install -D @swc/core @swc/cli// .swcrc
{
"module": {
"type": "commonjs"
},
"jsc": {
"parser": {
"syntax": "ecmascript"
}
}
}This compiles import to require(), similar to Babel but significantly faster.
Fix 7: Use Dynamic import() as a Workaround
If you’re in a CommonJS file and need to load an ESM-only package, use dynamic import(). Unlike the import declaration, import() is a function that works in both CJS and ESM.
// In a CommonJS file (.cjs or .js without "type": "module")
async function main() {
const { default: chalk } = await import('chalk');
console.log(chalk.green('It works!'));
}
main();This is the primary workaround when a dependency has migrated to ESM-only (like chalk, node-fetch, got, and many others) but your project is still CommonJS.
Common Mistake: You cannot use
await import()at the top level in a CJS file. Top-levelawaitonly works in ES modules. In CJS, you must wrap it in anasyncfunction. Forgetting this leads to anotherSyntaxError, which sends you down a frustrating rabbit hole.
Some patterns for common ESM-only packages:
// node-fetch v3 (ESM-only)
const fetch = (...args) =>
import('node-fetch').then(({ default: fetch }) => fetch(...args));
// ora (ESM-only)
async function showSpinner() {
const { default: ora } = await import('ora');
const spinner = ora('Loading...').start();
return spinner;
}Note: If many of your dependencies are ESM-only, constantly using import() becomes cumbersome. At that point, migrating your project to ESM (Fix 1) is the better long-term solution.
Fix 8: Handle ESM in Monorepos and Workspaces
Monorepos add complexity because each package can have its own module type. The "type" field in package.json only affects files within that package’s directory.
monorepo/
├── package.json # root — no "type" field
├── packages/
│ ├── core/
│ │ ├── package.json # "type": "module"
│ │ └── index.js # ESM ✓
│ ├── utils/
│ │ ├── package.json # no "type" field — defaults to CJS
│ │ └── index.js # CJS
│ └── app/
│ ├── package.json # "type": "module"
│ └── server.js # ESM ✓The error often appears when:
- Package A (ESM) imports Package B (CJS) — this usually works fine
- Package A (CJS) imports Package B (ESM) — this throws the error
The fix is to either:
- Make both packages use the same module type
- Use dynamic
import()in the CJS package to load the ESM package - Configure the ESM package to provide a CJS build via the
exportsfield:
{
"name": "@myorg/core",
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}This dual package approach lets the same package work whether it’s imported via import or require(). Build tools like tsup, unbuild, or rollup can generate both output formats from the same source.
If your monorepo’s module resolution is causing cascading import errors, the guide on fixing Node.js module not found errors covers dependency hoisting and resolution issues in depth.
Fix 9: Fix ESLint Configuration for ESM
ESLint configuration files themselves can trigger this error. If your .eslintrc.js or eslint.config.js uses import but your project is CJS, ESLint crashes on startup.
Flat Config (ESLint 9+)
ESLint 9 uses flat config files (eslint.config.js). If your project has "type": "module":
// eslint.config.js — ESM
import js from '@eslint/js';
export default [
js.configs.recommended,
{
rules: {
'no-unused-vars': 'warn',
},
},
];If your project does not have "type": "module", rename to eslint.config.mjs:
// eslint.config.mjs — always treated as ESM
import js from '@eslint/js';
export default [
js.configs.recommended,
];Legacy Config
For legacy .eslintrc.js, use module.exports:
// .eslintrc.js — CommonJS
module.exports = {
extends: ['eslint:recommended'],
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module', // tells ESLint your SOURCE files use ESM
},
};Setting sourceType: 'module' in parserOptions tells ESLint to parse your source files as modules. This is different from the config file itself being ESM. If ESLint is throwing parsing errors on your code, the guide on fixing ESLint parsing errors covers more scenarios.
Fix 10: Node.js --input-type Flag for Piped Code
When you pipe JavaScript code to Node.js via stdin, Node defaults to CJS mode:
# This fails
echo "import path from 'path'; console.log(path.sep);" | nodeUse the --input-type flag:
echo "import path from 'path'; console.log(path.sep);" | node --input-type=moduleSimilarly, when using node -e or node --eval:
# Fails
node -e "import fs from 'fs'"
# Works
node --input-type=module -e "import fs from 'fs'; console.log(fs.existsSync('.'))"Still Not Working?
If you’ve tried the fixes above and still see this error, work through these less obvious causes:
Check Your Node.js Version
ESM support evolved significantly across Node.js versions:
- Node 12: Experimental ESM behind
--experimental-modulesflag - Node 14: ESM stable, but many edge cases
- Node 16+: Full ESM support including
Node16module resolution - Node 18+: Native
fetch, improved JSON import support - Node 22+:
require()of ESM modules supported (experimental)
Check your version:
node --versionIf you’re on Node 12 or 14, upgrade. Many ESM features you’re trying to use require Node 16 or newer.
Inspect the Actual Error Location
Sometimes the error isn’t in your code — it’s in a dependency. Read the stack trace carefully:
SyntaxError: Cannot use import statement outside a module
at wrapSafe (node:internal/modules/cjs/loader:1281:20)
at Module._compile (node:internal/modules/cjs/loader:1321:27)
at node_modules/some-package/dist/index.js:1:1If the error points to node_modules, the package ships ESM code but your project tries to load it as CJS. Solutions:
- Use dynamic
import()(Fix 7) - Switch your project to ESM (Fix 1)
- Find an older version of the package that still supports CJS
- Use a compatibility wrapper like
esm(though this package is now unmaintained)
Check for Conflicting Configuration
Multiple tools reading module config can conflict. Your package.json might say "type": "module" while your tsconfig.json says "module": "commonjs". These must agree.
A consistent ESM setup looks like:
| File | Setting |
|---|---|
package.json | "type": "module" |
tsconfig.json | "module": "Node16" |
tsconfig.json | "moduleResolution": "Node16" |
| File extensions | .js for ESM, .cjs for any CJS files |
A consistent CJS setup looks like:
| File | Setting |
|---|---|
package.json | No "type" field (or "type": "commonjs") |
tsconfig.json | "module": "commonjs" |
tsconfig.json | "moduleResolution": "node" |
| File extensions | .js for CJS, .mjs for any ESM files |
Verify Bundler Processing Order
If you use Vite or Webpack and still see this error, the file might not be going through the bundler at all. This happens when:
- You run a file directly with
nodeinstead of through the bundler’s dev server - A script in
package.jsoncallsnode script.jsinstead of using the bundler - A server-side file is outside the bundler’s scope
For Vite-specific import issues, see fixing Vite import resolution errors.
JSON Import Errors
If the error occurs when importing JSON files, ESM handles JSON differently from CJS:
// CJS — works
const data = require('./data.json');
// ESM — fails without import attribute
import data from './data.json';
// ESM — correct (Node 17.5+)
import data from './data.json' with { type: 'json' };If your JSON parsing is failing with unexpected token errors instead, the guide on fixing JSON parse errors covers those cases.
Clear Build Caches
Stale caches can cause phantom errors after you’ve already fixed your config:
# Delete common caches
rm -rf node_modules/.cache
rm -rf dist
rm -rf .tsbuildinfo
# Reinstall dependencies
rm -rf node_modules
npm install
# Rebuild
npm run buildSome build tools cache the module type of files. After changing "type" in package.json or renaming file extensions, always clear caches and rebuild.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Error Cannot find module in Node.js (MODULE_NOT_FOUND)
How to fix 'Error: Cannot find module' and 'MODULE_NOT_FOUND' in Node.js. Covers missing packages, wrong import paths, node_modules issues, TypeScript moduleResolution, ESM vs CJS, and monorepo hoisting.
Fix: FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
How to fix the JavaScript heap out of memory error by increasing Node.js memory limits, fixing memory leaks, and optimizing builds in webpack, Vite, and Docker.
Fix: npm ERR! enoent ENOENT: no such file or directory
How to fix the npm ENOENT no such file or directory error caused by missing package.json, wrong directory, corrupted node_modules, broken symlinks, and npm cache issues.
Fix: TypeScript Property does not exist on type (TS2339)
How to fix TypeScript error TS2339 'Property does not exist on type'. Covers missing interface properties, type narrowing, optional chaining, intersection types, index signatures, type assertions, type guards, window augmentation, and discriminated unions.