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.

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.

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.

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.

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