You wrote an import statement and the runtime refused to parse it. In Node.js:
/app/report.js:2
import slugify from 'slugkit';
^^^^^^
SyntaxError: Cannot use import statement outside a moduleIn Chrome or Edge it appears in the console as Uncaught SyntaxError: Cannot use import statement outside a module; Firefox says SyntaxError: import declarations may only appear at top level of a module. The message means the file was loaded as a CommonJS module (Node) or a classic script (browser), and import/export declarations are only valid in files loaded as ES modules. The fix is to tell the runtime which kind of file this is, or to use the other system’s syntax consistently.
Quick fix checklist
- Node, whole project in ESM: add
"type": "module"to the nearestpackage.json, then replacerequireand__dirnameusages. - Node, one file: rename it to
.mjs(ESM) or keep.jsand switch torequire(CommonJS). - Check for an explicit
"type": "commonjs"inpackage.json; it forces.jsfiles to CommonJS. - Loading an ESM-only package from CommonJS: use
require()on Node 20.19+/22.12+, orawait import()everywhere else. - Browser: load the entry file with
type="module"on its script tag, and serve it over HTTP, notfile://. - Jest or TypeScript: check whether the tool compiles to CommonJS; the error often comes from its config, not your code.
Before you start
JavaScript has two module systems. CommonJS (CJS) is Node’s original one: require(), module.exports, __dirname, loaded synchronously. ES modules (ESM) are the language standard: import/export, always strict mode, import.meta, and the only module system browsers understand. You should know which one your project uses today and which Node version runs it (node -v); several rules below changed in recent releases.
Why it happens
The engine has to know a file’s module type before parsing it, because the two systems have different grammar. import declarations are only part of the module grammar; in a CommonJS file they are a syntax error, just as require does not exist inside an ES module.
How Node decides, per file, in this order:
.mjsis always ESM,.cjsis always CommonJS.- For
.js, the nearestpackage.jsondecides:"type": "module"means ESM,"type": "commonjs"means CommonJS. - If there is no
typefield, older Node versions treat.jsas CommonJS. Since Node 22.7 (and 20.19 on the 20.x line), Node first tries CommonJS and, if the file contains ESM syntax, re-parses it as ESM. When apackage.jsonexists withouttype, it prints aMODULE_TYPELESS_PACKAGE_JSONwarning asking you to add the field.
So on a current Node, you see this error mainly when "type": "commonjs" is set explicitly, when the file is .cjs, when a tool loads your code through its own CommonJS pipeline, or when you run an older Node version. With an explicit CommonJS setting, Node 22 also prints a hint first:
(node:4127) Warning: Failed to load the ES module: /app/report.js. Make sure to set "type": "module"
in the nearest package.json file or use the .mjs extension.Mixing both syntaxes produces the mirror image. If detection turns a file into ESM because it contains import, any require in the same file fails:
ReferenceError: require is not defined in ES module scope, you can use import insteadBrowsers treat a plain script tag as a classic script. Only a tag with type="module" is parsed as a module, and module scripts are fetched with CORS, so opening the page via file:// fails with a CORS error instead. Use a local dev server.
ESM from CommonJS. require() is synchronous, and ES modules may use top-level await, so for years require() of an ES module threw ERR_REQUIRE_ESM. Node 22.12 and 20.19 enabled require(esm) by default for modules without top-level await (early releases printed an ExperimentalWarning). A module that does use top-level await still throws ERR_REQUIRE_ASYNC_MODULE, and on older versions the only option is dynamic import(), which returns a promise and works from CommonJS.
Step-by-step walkthrough
Step 1: Identify which loader parsed the file
Read the stack under the error. Frames like at wrapSafe (node:internal/modules/cjs/loader...) mean Node’s CommonJS loader parsed it. Frames from jest-runtime or ts-node mean a tool did. In the browser, check the script tag that loaded the file. Then run node -v, because the answer to “will Node detect ESM syntax” depends on the version.
Step 2: Check what Node thinks the file is
Look at the extension and walk up from the file to the first package.json; that file’s type field applies (a nested package.json in a subfolder overrides the root one). A forgotten "type": "commonjs" in a nested package, or a build output folder with its own package.json, is a frequent surprise.
Step 3: Pick one module system per file and make it explicit
For a new or small project, prefer ESM: add "type": "module", use import throughout, and rename any file that must stay CommonJS to .cjs. For a large CommonJS codebase, keep CommonJS and use .mjs for individual ESM files, or load ESM dependencies with require() (recent Node) or import().
Step 4: Replace CommonJS-only globals in ESM files
ES modules have no __dirname, __filename, require or module. The replacements:
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { createRequire } from 'node:module';
console.log(import.meta.dirname === dirname(fileURLToPath(import.meta.url)));
console.log(typeof import.meta.filename);
const require = createRequire(import.meta.url); // only for loading CommonJS-only files
console.log(typeof require('node:path').join);
// true
// string
// functionimport.meta.dirname and import.meta.filename exist from Node 20.11 and 21.2; on older versions use the fileURLToPath form.
Step 5: Align your tools
Tools often transform code before Node sees it. TypeScript emits require when module is set to commonjs; with "module": "nodenext" it follows each file’s real type from package.json and extensions, which keeps compiler output and runtime in agreement. Jest runs tests as CommonJS by default, so an ESM-only dependency in node_modules can raise this exact error inside Jest even though the app works; the usual fixes are letting the transformer process that package or enabling Jest’s ESM mode (check the current Jest documentation, as its ESM support has been evolving). In the browser, bundlers like Vite handle ESM natively.
Worked scenario
A reporting script lives in a project whose package.json contains "type": "commonjs". A developer adds an ESM-only slug library and copies the import line from its README:
const path = require('node:path');
import slugify from 'slugkit';
const title = 'Q3 Sales Report';
console.log(path.join(__dirname, 'out', `${slugify(title)}.csv`));Running node report.js on Node 22 fails with SyntaxError: Cannot use import statement outside a module on line 2.
Diagnosis. The type field forces .js to CommonJS, so the import declaration is a syntax error. Simply switching the project to "type": "module" would break the require on line 1, __dirname on line 5 and every other CommonJS file in the project. The library being ESM-only is the real constraint.
Fix A: stay in CommonJS and load the package with require() on Node 20.19+ or 22.12+. The ES module’s default export appears as the default property:
const path = require('node:path');
const { default: slugify } = require('slugkit'); // Node 20.19+ / 22.12+
const title = 'Q3 Sales Report';
console.log(path.basename(path.join(__dirname, 'out', `${slugify(title)}.csv`)));
// q3-sales-report.csvFix B: stay in CommonJS and support older Node with dynamic import(), which is asynchronous:
const path = require('node:path');
async function main() {
const { default: slugify } = await import('slugkit'); // works in every supported Node version
const title = 'Q3 Sales Report';
console.log(path.basename(path.join(__dirname, 'out', `${slugify(title)}.csv`)));
}
main();
// q3-sales-report.csvFix C: make this file ESM by renaming it to report.mjs, which leaves the rest of the project alone:
import path from 'node:path';
import slugify from 'slugkit';
const title = 'Q3 Sales Report';
console.log(path.basename(path.join(import.meta.dirname, 'out', `${slugify(title)}.csv`)));
// q3-sales-report.csvThe team chose C for new scripts and planned a gradual migration of the project to "type": "module".
Common mistake
Adding "type": "module" and walking away. It fixes the one file you were looking at and turns every other require, module.exports and __dirname in the package into a runtime error. Do it deliberately, run the whole test suite, and rename files that must stay CommonJS to .cjs.
Downgrading the dependency forever to its last CommonJS version to avoid the error. That works short term but leaves you on an unmaintained release; require(esm) or import() solves the actual problem.
Writing const x = import('pkg') without await. Dynamic import() returns a promise, so x is a promise, and calling x() fails with x is not a function.
Verify the behavior
Confirm what Node supports and that each entry point actually runs:
node -v
node -p "process.features.require_module" # true where require(esm) is enabled
node report.mjsThen test the module boundary itself in an ESM test file:
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import slugify from 'slugkit';
const require = createRequire(import.meta.url);
assert.equal(slugify('Q3 Sales Report'), 'q3-sales-report');
assert.equal(require('slugkit').default, slugify); // same module instance via require(esm)
console.log('import and require agree');
// import and require agreeThe second assertion only passes on Node versions with require(esm); on older ones it throws ERR_REQUIRE_ESM, which is itself a useful signal about your runtime. In the browser, reload with DevTools open and confirm the Network panel shows the module script loading with status 200 and no syntax error in the console.
Interview exercise
“What is the difference between how CommonJS and ES modules are loaded, and why couldn’t require() load ES modules for so long?”
Answer and reasoning
CommonJS is evaluated synchronously at the moment require() runs: Node reads the file, wraps it in a function that receives require, module, exports, __dirname and __filename, executes it and returns module.exports. Exports are a plain object, determined only after execution. ES modules are loaded in phases: the whole import graph is parsed and linked first (so imports and exports are known statically and bindings are live), and only then evaluated, and evaluation may be asynchronous because of top-level await. A synchronous require() cannot wait for an asynchronous graph, which is why Node refused with ERR_REQUIRE_ESM. Newer Node versions resolved this pragmatically: require() now loads an ES module synchronously when its graph has no top-level await, and throws ERR_REQUIRE_ASYNC_MODULE when it does. Going the other way was always possible, since import can load CommonJS by running it and exposing module.exports as the default export. A strong answer also mentions the consequences: ESM’s static structure enables tree shaking and early errors for missing exports, while CommonJS allows conditional, dynamic require calls.
Continue learning
Review more module questions in the JavaScript interview questions and the JavaScript MCQs. How ES module bindings stay connected to their exporters is explained in module live bindings, and the require is not defined variant is covered in ReferenceError: x is not defined. Official references: Node’s ECMAScript modules documentation, determining the module system, loading ES modules with require(), and MDN’s JavaScript modules guide.