Fix 'Cannot use import statement outside a module' in Node.js
You're happily coding away, adding a modern package to your Node.js project, and then—boom:
SyntaxError: Cannot use import statement outside a module
Frustrating, right? This error is one of the most common headaches for developers moving to modern JavaScript. The good news? It's easy to fix once you understand what's happening under the hood.
In this article, we'll explore why this error occurs, walk through three practical solutions, and cover common pitfalls so you can get back to coding.
Why This Error Matters
If you've ever tried to use ES Modules (the import/export syntax) in Node.js, you've probably encountered this error. It's especially common when:
- Adding modern npm packages that distribute ES Module code
- Migrating from frontend codebases where ES Modules are the default
- Following tutorials written for bundlers or browsers
- Starting new projects and wanting modern syntax from day one
Node.js has supported ES Modules since version 12, but it still defaults to CommonJS for .js files. This backward-compatibility decision means modern syntax doesn't work out of the box—unless you explicitly opt in.
Understanding this distinction isn't just about fixing an error; it's about navigating the evolving JavaScript ecosystem confidently.
Understanding the Problem: CommonJS vs ES Modules
Before diving into solutions, let's clarify what's actually happening.
CommonJS: The Node.js Default
CommonJS is the original module system for Node.js. It uses require() and module.exports:
// CommonJS syntax
const express = require('express');
module.exports = { myFunction };
Node.js treats all .js files as CommonJS by default. This has been the standard since Node.js was created, and millions of packages use it.
ES Modules: The Modern Standard
ES Modules (ESM) are the official JavaScript module system, standardized in ES6. They use import and export:
// ES Module syntax
import express from 'express';
export { myFunction };
Browsers and modern bundlers (like Vite, esbuild) use ES Modules by default. Many newer packages publish ESM-first or ESM-only code.
The Conflict
When you write import ... in a .js file, Node.js tries to parse it as CommonJS (because that's the default). But import isn't valid CommonJS syntax—hence the error.
Think of it as speaking Spanish to someone who only speaks German. The grammar rules don't match, and communication breaks down.
Solution 1: Add "type": "module" to package.json
This is the most common and recommended solution for projects fully embracing ES Modules.
How It Works
Add "type": "module" to your package.json:
{
"name": "my-awesome-project",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node index.js"
}
}
Now Node.js treats all .js files in your project as ES Modules. Your import statements will work:
// index.js - now works!
import fs from 'fs';
import { readConfig } from './config.js'; // Note: .js extension required!
console.log('Hello, ES Modules!');
Important Notes
1. Use .js extensions for local imports:
// ❌ Won't work
import { helper } from './utils';
// ✅ Works
import { helper } from './utils.js';
ES Modules require explicit file extensions for local imports. This is different from CommonJS, where you could omit extensions.
2. require() stops working:
If you switch to "type": "module", you can't use require() anymore. Use import throughout, or see the mixed modules section below.
3. __dirname and __filename aren't available:
These CommonJS globals don't exist in ES Modules. Instead, use:
// ES Module equivalent
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
When to Use This Solution
- ✅ New projects starting fresh
- ✅ Projects fully migrating to ES Modules
- ✅ When you control the codebase and can refactor
Solution 2: Use the .mjs File Extension
If you only need ES Modules in specific files, or you're working with mixed module types, the .mjs extension is your friend.
How It Works
Simply rename your file from .js to .mjs:
// utils.mjs - automatically treated as ES Module
export function greet(name) {
return `Hello, ${name}!`;
}
// main.js - can import from .mjs
// If main.js is CommonJS, use dynamic import (see Solution 3)
// If main.js is ESM (via package.json), use:
import { greet } from './utils.mjs';
Node.js treats .mjs files as ES Modules regardless of your package.json settings. Conversely, .cjs files are always treated as CommonJS.
When to Use This Solution
- ✅ Single files that need ES Modules in a CommonJS project
- ✅ Gradual migration strategies
- ✅ Test files using modern syntax
- ✅ When you need both CommonJS and ES Module versions
Example: Mixed Extensions
project/
├── package.json (no "type" field → CommonJS)
├── index.cjs # CommonJS entry point
├── utils.mjs # ES Module utilities
├── legacy.cjs # Legacy CommonJS code
└── modern.mjs # Modern ES Module code
Solution 3: Dynamic Imports
Sometimes you need to use an ES Module from within a CommonJS file. Since require() can't load ES Modules, dynamic imports come to the rescue.
How It Works
The import() function works like a promise-based require():
// CommonJS file using an ES Module
async function main() {
const { default: chalk } = await import('chalk');
const { someFunction } = await import('./esm-module.mjs');
console.log(chalk.blue('Hello from dynamic import!'));
someFunction();
}
main().catch(console.error);
Key Points
-
import()returns a Promise, so useawaitor.then() - The imported module's default export is available as
.default - Works in both CommonJS and ES Modules
- Great for conditional loading and code splitting
When to Use This Solution
- ✅ Loading ES Module packages in CommonJS projects
- ✅ Conditional imports based on runtime conditions
- ✅ Lazy loading for better startup performance
- ✅ Gradual migration without changing project structure
Example: Using a Modern Package in CommonJS
// legacy-app.cjs
const fs = require('fs');
async function processData() {
// Node's built-in 'fs/promises' is ESM
const { readFile } = await import('fs/promises');
// Modern package that's ESM-only
const { marked } = await import('marked');
const content = await readFile('README.md', 'utf-8');
const html = await marked(content);
return html;
}
processData().then(console.log);
Common Pitfalls and How to Avoid Them
Pitfall 1: Missing File Extensions
The Problem: ES Modules require explicit extensions for local imports.
// ❌ This fails silently or throws
import { config } from './config';
// ✅ Always include the extension
import { config } from './config.js';
The Fix: Get in the habit of adding .js (or .mjs) to every local import. Tools like ESLint with the import/extensions rule can catch this.
Pitfall 2: Mixing require and import
The Problem: You can't use require() inside an ES Module.
// ❌ Won't work in ESM
const lodash = require('lodash');
The Fix: Either convert everything to import, or use dynamic imports for specific cases:
// ✅ Option 1: Convert to import
import lodash from 'lodash';
// ✅ Option 2: Dynamic import when needed
const lodash = await import('lodash');
Pitfall 3: __dirname and __filename Are Undefined
The Problem: These globals don't exist in ES Modules.
// ❌ ReferenceError
console.log(__dirname);
The Fix: Create them manually:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname); // Now works!
Pitfall 4: JSON Files Can't Be Imported Directly
The Problem: import config from './config.json' requires an assertion in Node.js.
// ❌ May fail or require flags
import config from './config.json';
The Fix: Use import assertions (Node.js 16.14+):
// ✅ With import assertion
import config from './config.json' with { type: 'json' };
// ✅ Alternative: Use fs
import { readFile } from 'fs/promises';
const config = JSON.parse(await readFile('./config.json', 'utf-8'));
Real-World Scenarios
Scenario 1: Migrating an Existing Project to ES Modules
Step-by-step approach:
Add
"type": "module"to package.jsonRename CommonJS files to
.cjs:
# Files that still use require()
mv database.js database.cjs
mv legacy-utils.js legacy-utils.cjs
- Update imports in ESM files:
// Add .js extensions
import { db } from './database.cjs'; // Works! Node can handle .cjs
Replace
__dirnameand__filename:
Add the helper snippet to any files that need it.Test thoroughly: Some packages may behave differently under ESM.
Scenario 2: Working with Mixed Modules
When your project has both CommonJS and ES Modules:
my-project/
├── package.json # "type": "module"
├── src/
│ ├── index.js # ES Module (uses import)
│ ├── modern.mjs # ES Module (explicit)
│ └── legacy.cjs # CommonJS (uses require)
// index.js (ES Module)
import { modernFunction } from './modern.mjs';
// Use dynamic import for CommonJS interop
const { legacyFunction } = await import('./legacy.cjs');
export async function run() {
const result = legacyFunction();
return modernFunction(result);
}
Scenario 3: Using ESM-Only Packages in CommonJS Projects
Some modern packages (like chalk v5+, node-fetch v3+) are ESM-only. In a CommonJS project:
// ❌ Won't work
const chalk = require('chalk'); // Error: require() of ES Module
// ✅ Use dynamic import
const chalk = await import('chalk').then(m => m.default);
// ✅ Or use an older CommonJS version
// npm install chalk@4
const chalk = require('chalk'); // Works with v4
FAQ / Troubleshooting
Q: Should I use ES Modules or CommonJS for new projects?
A: ES Modules are the modern standard and future of JavaScript. For new projects, use "type": "module" unless you have a specific reason to stick with CommonJS (like legacy dependencies or specific tooling requirements).
Q: Can I use both in the same project?
A: Yes! Use .mjs for ES Module files and .cjs for CommonJS files. You can also use dynamic import() to load ES Modules from CommonJS code.
Q: Why do my tests fail after switching to ES Modules?
A: Your test runner may need configuration. Jest requires transform: {} or specific ESM flags. Mocha needs --experimental-vm-modules. Check your test framework's ESM documentation.
Q: What about TypeScript?
A: TypeScript transpiles to your chosen module format. Set "module": "ES2022" or "module": "NodeNext" in tsconfig.json, and let the compiler handle the output.
Q: My import works in development but fails in production!
A: Check your build process. Bundlers like webpack or esbuild may handle extensions differently. Ensure consistency between your development and build configurations.
Conclusion
The "Cannot use import statement outside a module" error is a rite of passage in modern JavaScript development. It's not a bug—it's Node.js protecting backward compatibility while supporting the modern module system.
Quick reference for the fix:
| Situation | Solution |
|---|---|
| New project, all ES Modules | Add "type": "module" to package.json |
| Mixed module types | Use .mjs and .cjs extensions |
| CommonJS loading ES Module | Use dynamic import()
|
| Need ESM in one file | Rename to .mjs
|
The JavaScript ecosystem is evolving toward ES Modules. Understanding these options now will save you debugging time and make your code more portable across environments.
Next time you see this error, you'll know exactly what to do. Happy coding! 🚀
Top comments (0)