DEV Community

AttractivePenguin
AttractivePenguin

Posted on

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

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

If you've ever tried to use ES modules in Node.js and been greeted with this error:

SyntaxError: Cannot use import statement outside a module
Enter fullscreen mode Exit fullscreen mode

You're not alone. This is one of the most common errors developers encounter when modernizing their Node.js codebase. The good news? It's incredibly easy to fix. Let me show you exactly how.

Why This Error Happens

Node.js has been around since 2009, and for most of its history, it used CommonJS as its module system. You're probably familiar with it:

// CommonJS - the old way
const express = require('express');
const { something } = require('./myModule');

module.exports = myFunction;
Enter fullscreen mode Exit fullscreen mode

But modern JavaScript uses ES modules (ESM):

// ES Modules - the modern way
import express from 'express';
import { something } from './myModule.js';

export default myFunction;
Enter fullscreen mode Exit fullscreen mode

The problem? By default, Node.js treats .js files as CommonJS modules. When it sees an import statement in a CommonJS context, it throws the error. This default behavior exists for backward compatibility with the millions of existing Node.js projects written in CommonJS.

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

This is the cleanest solution for new projects or projects fully migrating to ES modules.

Step 1: Open or create your package.json file.

Step 2: Add the "type" field:

{
  "name": "my-awesome-project",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node index.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

That's it! Now all .js files in your project are treated as ES modules.

// index.js - now this works!
import express from 'express';

const app = express();
app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Important note: When using ES modules, you must include the .js extension in local imports:

// ❌ This won't work
import { myFunc } from './utils';

// ✅ This works
import { myFunc } from './utils.js';
Enter fullscreen mode Exit fullscreen mode

Solution 2: Use the .mjs Extension

If you can't modify package.json (perhaps you're adding a single ESM file to an existing CommonJS project), use the .mjs extension:

// myfile.mjs - Node.js treats this as ES module automatically
import fs from 'fs';
import { parse } from './parser.js';

export const result = parse(fs.readFileSync('data.json'));
Enter fullscreen mode Exit fullscreen mode

You can run it directly:

node myfile.mjs
Enter fullscreen mode Exit fullscreen mode

This approach is great for:

  • Mixed projects with both CommonJS and ES modules
  • Migration projects where you're gradually transitioning files
  • Testing ESM in legacy codebases

Solution 3: Use Dynamic Imports

Sometimes you need ES modules in a CommonJS file. That's where dynamic imports come in:

// CommonJS file using dynamic import
const myModule = await import('./esm-module.mjs');

// Access the default export
const result = myModule.default;

// Or named exports
const { helper } = myModule;
Enter fullscreen mode Exit fullscreen mode

Dynamic imports return a promise, so handle them appropriately:

// Using .then()
import('./myModule.mjs').then((module) => {
  module.doSomething();
});

// Using async/await
async function init() {
  const module = await import('./myModule.mjs');
  return module.doSomething();
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls and How to Avoid Them

1. Forgetting the .js Extension

ES modules require explicit file extensions for local imports. This isn't optional—it's how the spec works.

// ❌ Browser and bundlers might handle this, but Node.js won't
import { foo } from './bar';

// ✅ Always include the extension
import { foo } from './bar.js';
Enter fullscreen mode Exit fullscreen mode

2. Mixing require() and import in the Same File

You can't mix them in the same file:

// ❌ This will fail
import express from 'express';
const lodash = require('lodash'); // SyntaxError!
Enter fullscreen mode Exit fullscreen mode

Choose one module system per file, or use dynamic imports for CommonJS dependencies.

3. Using __dirname and __filename in ESM

These CommonJS globals don't exist in ES modules. Here's the replacement:

// CommonJS
console.log(__dirname);
console.log(__filename);

// ES Modules
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Enter fullscreen mode Exit fullscreen mode

4. Third-party Packages Without ESM Exports

Some older packages only support CommonJS. Most work fine, but a few need special handling:

// If a package doesn't have ESM exports, try default import
import pkg from 'old-package';
const { named, exports } = pkg;

// Or use dynamic import
const oldPkg = await import('old-package');
Enter fullscreen mode Exit fullscreen mode

Real-World Scenarios

Migrating a Large Project

Don't try to migrate everything at once. Use this incremental approach:

  1. Start with new files: Write all new files as .mjs
  2. Use dynamic imports: Bridge CommonJS and ESM during migration
  3. Convert package by package: Isolate modules and convert them
  4. Switch "type": "module": Once most files are ESM, enable it globally
// During migration - CommonJS entry point
const main = async () => {
  const app = await import('./app.mjs');
  await app.default.start();
};
main();
Enter fullscreen mode Exit fullscreen mode

Working with TypeScript

TypeScript adds another layer. Configure it correctly:

// tsconfig.json
{
  "compilerOptions": {
    "module": "ES2022",
    "moduleResolution": "node",
    "target": "ES2022"
  }
}
Enter fullscreen mode Exit fullscreen mode

Your package.json still needs "type": "module" if you compile to .js files.

FAQ / Troubleshooting

Q: Should I migrate to ES modules?

A: For new projects, absolutely. For existing projects, it depends on your dependencies. If all your dependencies support ESM, go for it. The benefits include better tree-shaking, standard syntax, and future-proofing.

Q: Why does my import work in browsers but not Node.js?

A: Browsers are more forgiving about missing file extensions. Build tools like webpack, Vite, and esbuild also handle this. Node.js follows the ESM spec strictly, requiring explicit extensions.

Q: Can I use require() in an ES module?

A: Not directly. However, you can create a require function:

import { createRequire } from 'module';
const require = createRequire(import.meta.url);

const cjsModule = require('./commonjs-module.cjs');
Enter fullscreen mode Exit fullscreen mode

Use this sparingly—it's meant for transitional code.

Q: What about module.exports in ES modules?

A: You can't use module.exports syntax in ESM. Use named exports or export default:

// CommonJS
module.exports = { a, b };
module.exports.c = c;

// ES Modules equivalent
export { a, b };
export const c = 'value';
// Or
export default { a, b, c };
Enter fullscreen mode Exit fullscreen mode

Conclusion

The "Cannot use import statement outside a module" error is Node.js's way of protecting backward compatibility. Three solutions solve it:

  1. "type": "module" — Best for new projects
  2. .mjs extension — Best for mixed projects
  3. Dynamic imports — Best for CommonJS needing ESM

The JavaScript ecosystem is moving toward ES modules as the standard. Modern packages like Express 5.x, Node-fetch v3, and most new libraries require ESM. Understanding how to work with ES modules in Node.js isn't just about fixing errors—it's about writing future-proof code.

Start with "type": "module" in your next project. Remember to include file extensions. And if you hit issues, you now know exactly how to debug them.

Happy coding!

Top comments (0)