SyntaxError: Cannot use import statement outside a module means JavaScript found an import in a file it is treating as an old-style script. The fix depends on where the code runs: in Node you set the module type, in the browser you add type="module", and in a test runner you configure the transform. All four are below.
Why this happens#
JavaScript has two module systems. CommonJS came first and uses require() and module.exports; it is what Node used for years. ES modules are the newer standard and use import and export.
A file is one or the other, never both, and the runtime has to decide which before it parses a single line. When it guesses CommonJS and then meets import, you get this error.
Fix 1: Node#
Add "type": "module" to your package.json:
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"main": "index.js"
}
Every .js file in the project is now treated as an ES module. If you have no package.json, create one with npm init -y and add the line.
For a single file without touching the project, rename it:
mv script.js script.mjs
node script.mjs
.mjs always means ES module; .cjs always means CommonJS, whatever the package.json says.
What changes when you switch#
ES modules are stricter in a few ways that catch people out:
// File extensions are required for local imports
import { helper } from "./utils.js"; // correct
import { helper } from "./utils"; // ERR_MODULE_NOT_FOUND
// __dirname and __filename do not exist
import { fileURLToPath } from "url";
import path from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// JSON needs an import attribute
import data from "./data.json" with { type: "json" };
You can still load a CommonJS package from an ES module, because the default export is the whole module.exports object:
import express from "express"; // works fine
Fix 2: the browser#
<!-- Fails: import in a classic script -->
<script src="app.js"></script>
<!-- Works -->
<script type="module" src="app.js"></script>
Two consequences of type="module" that surprise people:
- Module scripts are deferred automatically, so they run after the HTML is parsed. You no longer need to wrap everything in a DOMContentLoaded handler.
- Modules are subject to CORS, so opening the page with
file://fails. Serve it over HTTP:npx serveorpython -m http.server.
Inline modules work too:
<script type="module">
import { greet } from "./greet.js";
greet();
</script>
Fix 3: TypeScript#
TypeScript compiles import to whatever module says. If the output is ES modules but Node expects CommonJS, you get this error at runtime rather than at compile time.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist"
}
}
NodeNext tells TypeScript to follow the same rules Node does, using your package.json type field. That keeps the two in agreement, which is the actual fix.
For CommonJS output instead, set "module": "CommonJS" and leave "type": "module" out of package.json.
Fix 4: Jest#
Jest historically ran in CommonJS, so importing an ES-module-only package fails. The straightforward route is Babel:
npm install --save-dev babel-jest @babel/core @babel/preset-env
// babel.config.cjs
module.exports = {
presets: [["@babel/preset-env", { targets: { node: "current" } }]],
};
Alternatively, run Jest in native ESM mode:
{
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
}
}
Vitest is worth considering if you keep fighting this — it supports ES modules natively and its API is close enough that most Jest tests need few changes.
You cannot mix the two in one file#
import fs from "fs";
const path = require("path"); // ReferenceError: require is not defined
Pick one per file. If you genuinely need a CommonJS-only package inside an ES module:
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const legacy = require("some-old-package");
Quick diagnosis#
| Where it happens | Most likely fix |
|---|---|
node script.js |
Add "type": "module", or rename to .mjs |
| Browser console | Add type="module" to the script tag |
After tsc |
Set module to NodeNext |
| Only in tests | Configure babel-jest, or switch to Vitest |
| Only in one dependency | That package is ESM-only; use dynamic import() |
Questions people ask#
Should new projects use ES modules?
Yes. It is the standard, it works in browsers without a build step, and the ecosystem has largely moved. CommonJS is for maintaining existing code.
Why does the error mention “outside a module” when my file is a module?
Because the runtime disagrees. What matters is not what you intended but how the file was classified — by extension, by package.json, or by the script tag.
Can I import an ES-only package from CommonJS?
Not with require(). Use dynamic import, which returns a promise: const mod = await import("the-package"). That works inside an async function in CommonJS.
What is the difference between .mjs and .cjs?
They force the classification regardless of package.json. .mjs is always an ES module, .cjs is always CommonJS. Useful for one-off files and during a migration.
Where to go next#
- Exporting functions in JavaScript — named and default exports in practice.
- Why is my JavaScript not working? — a general debugging order.
- VS Code setup for beginners — catching these before you run the file.