Back to core workflows Fix dependency resolution Tune package metadata Jump to monorepo patterns

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

Exact symptoms and error messages Exact symptoms and error messages in production JavaScript package workflows. Exact symptoms and error messages Exact symptoms and error messages in production JavaScript package workflows.
Exact symptoms and error messages — the core idea of this section at a glance.
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.

Format signal How Node decides a file's module format. How is the file's format signalled? type: module .js parsed as ESM .mjs always ESM default .js parsed as CJS
Node reads type, extension and flags — the strictest wins.

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.

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 is always ESM, .cjs always CommonJS), 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. The format is a property of how the file is resolved, not of its contents.

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. Making the format signal explicit removes the ambiguity that produces the error.

Resolution and configuration patch

Pick one format signal and make it consistent:

Resolution and configuration patch Pick one format signal and make it consistent: Resolution and configuration patch Pick one format signal and make it consistent:
Resolution and configuration patch — the core idea of this section at a glance.
// 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.

Pick one format signal and make it consistent across the file's context:

// package.json — opt the whole package into ESM
{ "type": "module" }

Or rename the single file to .mjs, or give a tool config the matching loader/extension. For a config file specifically, an explicit extension is the most robust:

# force ESM or CommonJS regardless of the package type
mv postcss.config.js postcss.config.mjs   # ESM
mv postcss.config.js postcss.config.cjs   # CommonJS

Do not mix import in a .js file whose package is CommonJS; either flip the package type or use the explicit extension.

CLI validation and debug commands

CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows. CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows.
CLI validation and debug commands — the core idea of this section at a glance.
# 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/.cjs extensions when a package must contain both formats.
  • Keep tool configs in the extension their loader expects (.mjs for ESM configs).
  • Add a lint rule that flags import syntax in files resolved as CommonJS.
Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows. Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows.
Prevention and CI guardrails — the core idea of this section at a glance.
  • Set "type": "module" explicitly rather than relying on the implicit CommonJS default.
  • Use .mjs/.cjs extensions when a package must contain both formats.
  • Give tool configs the extension their loader expects (.mjs for ESM configs).
  • Add a lint rule that flags import syntax 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.

Module strategy Which module system to declare for the package. What is the package mostly? new / ESM-first type: module + .cjs legacy / CJS default + .mjs mixed no type declare a primary
Pick one system, declare it, and give the minority format an explicit extension.

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.

Config extension Pin config format with .mjs or .cjs to remove ambiguity. *.config.js parsed by package type rename .mjs / .cjs force the format tool loads it no guessing
An explicit config extension forces the parser and documents the format.

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.

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 with 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 goal is that a file's format is never in doubt, because the package type and any explicit extensions agree.

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. 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.
Choosing a project-wide module strategy — the core idea of this section at a glance.

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, producing this error in one place and its mirror require is not defined in another. Picking a primary system, declaring it explicitly with type, and treating the minority format as the one that carries an explicit extension eliminates a whole class of format-detection surprises. That single decision, made deliberately at the start, is worth more than any number of per-file fixes applied after the fact.

Config files and the module boundary

Tool configuration files are where this error 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, which is confusing because the config looks syntactically fine — it is simply being parsed under the wrong system.

Config files and the module boundary Tool configuration files are where this error bites hardest, because a config file's module system is decided by the sam Config files and the module boundary Tool configuration files are where this error bites hardest, because a config file's module system is decided by the same rules as your source.
Config files and the module boundary — the core idea of this section at a glance.

The robust fix is to make the config's format explicit with an extension. Naming it *.config.mjs forces ESM or *.config.cjs forces CommonJS, regardless of the package's type, and most modern tools resolve both. This removes any ambiguity about how the config will be parsed and documents, at a glance, which module system the file is written in. For a project that flips its package type — say, migrating to ESM — pinning config extensions explicitly means the configs do not silently break when the default format changes, which is a common and frustrating side effect of such a migration.

The mirror error: require is not defined in ESM

The counterpart to this error appears when the format is flipped: a file resolved as ESM that uses require throws ReferenceError: require is not defined, because require and module are CommonJS globals that do not exist in an ES module. It is the same root cause seen from the other side — a mismatch between the file's resolved format and the syntax it uses — and it appears when a package sets type: module but a file still uses CommonJS syntax, or when a .mjs file uses require.

Format mismatch Which error a format mismatch produces. File format vs syntax? CJS file, import cannot use import ESM file, require require not defined aligned no error
Both errors are the same mismatch — the fix is to align format and syntax.

The fixes mirror the ones for the import error. Convert the file to ESM syntax (import instead of require, import.meta.url instead of __dirname), or if the file must stay CommonJS, give it a .cjs extension so it is parsed as CommonJS regardless of the package type. Where you genuinely need require inside an ES module — to load a CommonJS dependency by path — createRequire from node:module builds one: const require = createRequire(import.meta.url). Recognizing that both errors are the same format-mismatch problem, and that the tools to resolve them (explicit extensions, a declared type, createRequire) are shared, turns two confusing errors into one well-understood boundary.

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.

Why does the error happen before my code runs?

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. Make the signal explicit with type or a .mjs/.cjs extension.

How do I fix this 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. Most modern tools resolve both, which removes the ambiguity about how the 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, producing this error in one place and require is not defined in another. Declare a primary system with type and give the minority format an explicit extension.

Why do I get 'require is not defined' after setting type: module?

Because the file is now parsed as ESM, where require and module do not exist. Convert it to import syntax, or give it a .cjs extension to keep it CommonJS. To load a CommonJS dependency by path from ESM, use createRequire(import.meta.url) from node:module.

Does a bundler have this problem too?

No — bundlers parse ESM syntax themselves regardless of the package type, so code that a bundler accepts can still throw this error when run directly by Node, which applies its format-detection rules strictly. Make the signal explicit so both agree.

Related

ESM and CJS Interoperability