DEV Community

AttractivePenguin
AttractivePenguin

Posted on

How to Fix 'Cannot use import statement outside a module' in Node.js

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
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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!');
Enter fullscreen mode Exit fullscreen mode

Important Notes

1. Use .js extensions for local imports:

// ❌ Won't work
import { helper } from './utils';

// ✅ Works
import { helper } from './utils.js';
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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}!`;
}
Enter fullscreen mode Exit fullscreen mode
// 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';
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Key Points

  • import() returns a Promise, so use await or .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);
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

Pitfall 3: __dirname and __filename Are Undefined

The Problem: These globals don't exist in ES Modules.

// ❌ ReferenceError
console.log(__dirname);
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

Real-World Scenarios

Scenario 1: Migrating an Existing Project to ES Modules

Step-by-step approach:

  1. Add "type": "module" to package.json

  2. Rename CommonJS files to .cjs:

   # Files that still use require()
   mv database.js database.cjs
   mv legacy-utils.js legacy-utils.cjs
Enter fullscreen mode Exit fullscreen mode
  1. Update imports in ESM files:
   // Add .js extensions
   import { db } from './database.cjs'; // Works! Node can handle .cjs
Enter fullscreen mode Exit fullscreen mode
  1. Replace __dirname and __filename:
    Add the helper snippet to any files that need it.

  2. 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)
Enter fullscreen mode Exit fullscreen mode
// 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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)