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
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;
But modern JavaScript uses ES modules (ESM):
// ES Modules - the modern way
import express from 'express';
import { something } from './myModule.js';
export default myFunction;
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"
}
}
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);
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';
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'));
You can run it directly:
node myfile.mjs
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;
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();
}
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';
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!
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);
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');
Real-World Scenarios
Migrating a Large Project
Don't try to migrate everything at once. Use this incremental approach:
-
Start with new files: Write all new files as
.mjs - Use dynamic imports: Bridge CommonJS and ESM during migration
- Convert package by package: Isolate modules and convert them
-
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();
Working with TypeScript
TypeScript adds another layer. Configure it correctly:
// tsconfig.json
{
"compilerOptions": {
"module": "ES2022",
"moduleResolution": "node",
"target": "ES2022"
}
}
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');
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 };
Conclusion
The "Cannot use import statement outside a module" error is Node.js's way of protecting backward compatibility. Three solutions solve it:
-
"type": "module"— Best for new projects -
.mjsextension — Best for mixed projects - 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)