Fixing 'Cannot use import statement outside a module'
Node or a tool throws SyntaxError: Cannot use import statement outside a module when it parses ESM syntax as CommonJS. This page covers the exact error, why the file is being parsed as CJS, and the three ways to fix the format signal.
Exact symptoms and error messages
import { render } from './app.js';
^^^^^^
SyntaxError: Cannot use import statement outside a module
at wrapSafe (node:internal/modules/cjs/loader:1281:20)
Root cause analysis
Node decides a file's module format from signals — the nearest package.json type field, the file extension, or a --input-type flag — and here every signal says CommonJS while the file contains import. The parser never reaches runtime; it rejects the syntax. This is the format-detection boundary documented in ESM and CJS Interoperability and driven by the type field in Understanding package.json Fields.
The error is a parse-time rejection, which is why it fires before any of your code runs. Node must decide a file's module system before it can parse it, and it does so from the nearest package.json type field, the file extension (.mjs always ESM, .cjs always CJS), or an explicit --input-type. When every signal says CommonJS but the source uses import, the parser aborts immediately — it never gets far enough to hit a runtime error.
This is also why the error is so common in tool configuration files. A postcss.config.js or eslint.config.js written with import lives in a package with no type: module, so the tool's loader parses it as CommonJS and rejects it. The file was never wrong syntactically — it was interpreted under the wrong module system, which is a configuration mismatch, not a code bug.
Resolution and configuration patch
Pick one format signal and make it consistent:
// package.json — opt the whole package into ESM
{
"type": "module"
}
Or rename the single file to .mjs, or (for a tool config) provide the matching loader. Do not mix import in a .js file whose package is CommonJS.
CLI validation and debug commands
# Ask Node how it will treat the file
node --input-type=module -e "import('./app.js').then(()=>console.log('ESM ok'))"
# Confirm the effective type
node -e "console.log(require('./package.json').type || 'commonjs')"
Prevention and CI guardrails
- Set
"type": "module"explicitly rather than relying on the implicit CommonJS default. - Use
.mjs/.cjsextensions when a package must contain both formats. - Keep tool configs in the extension their loader expects (
.mjsfor ESM configs). - Add a lint rule that flags
importsyntax in files resolved as CommonJS.
Choosing a project-wide module strategy
Rather than fixing each file as it errors, decide the package's module system once and make every signal agree. For new packages, type: module and plain .js files is the modern default: you write import everywhere and reach for .cjs only for the rare file that must be CommonJS. For a legacy package that is mostly CommonJS, leave the default and use .mjs for the specific files that need ESM.
The failure mode to avoid is a package with no type field and a mix of import and require in .js files — every tool then has to guess, and they guess differently. Pick a primary system, declare it explicitly with type, and treat the minority format as the one that carries an explicit extension. That single decision eliminates a whole class of 'cannot use import' and its mirror-image require is not defined errors.
Config files and the module boundary
Tool configs are where this bites hardest, because a config file's module system is decided by the same rules as your source. In a type: module package, a *.config.js is parsed as ESM and must use import/export; in a CommonJS package it must use require/module.exports. Mixing them produces exactly this error from the tool that loads the config.
The robust fix is to make the config's format explicit with an extension. Name it *.config.mjs to force ESM or *.config.cjs to force CommonJS, regardless of the package's type. Most modern tools resolve both, so pinning the extension removes any ambiguity about how the config will be parsed — and it documents, at a glance, which module system the file is written in.
Frequently Asked Questions
Why does the same file run in my bundler but not in Node?
Bundlers accept ESM syntax regardless of type because they parse it themselves. Node applies its format-detection rules strictly, so a mismatch that a bundler tolerates still throws at runtime.
Is .mjs or "type": "module" better?
"type": "module" opts the whole package into ESM and is cleanest for new packages. Use .mjs for a single ESM file inside an otherwise-CommonJS package.
Why does the error happen before my code runs?
Because it is a parse-time rejection. Node picks a file's module system before parsing, and if the signals say CommonJS while the source uses import, the parser aborts immediately — it never reaches runtime, so no code executes.
How do I fix it in a tool config file?
Give the config an explicit extension: *.config.mjs forces ESM, *.config.cjs forces CommonJS, regardless of the package's type. That removes the ambiguity about how the tool's loader parses it.
Should a mixed package declare a type?
Always. A package with no type and both import and require in .js files forces every tool to guess. Declare a primary system with type and give the minority format an explicit .mjs/.cjs extension.
Related
- ESM and CJS Interoperability — the module-format rules behind this error.