ES Modules in JavaScript

· 3 min read · Updated March 13, 2026 · beginner
javascript es6 modules import export

JavaScript modules let you break your code into reusable pieces. Instead of putting everything in one file, you can split related code into separate files and import what you need. This makes code easier to maintain, test, and understand.

When you first start writing JavaScript, everything goes in a single file. That works fine for small scripts. As your project grows, you find yourself scrolling through thousands of lines looking for one function. Modules solve this by letting you organize code into logical files.

The import and export Syntax

ES Modules use import and export keywords. You export things from a file to make them available to other files, then import them where needed.

// math.js - the exporting file
export function add(a, b) {
  return a + b;
}

export const PI = 3.14159;
// main.js - the importing file
import { add, PI } from './math.js';

console.log(add(2, 3)); // 5
console.log(PI); // 3.14159

The curly braces in the import are called “named imports.” They let you pull out specific exports by their name.

Default Exports

Each module can have one default export. Default exports don’t need curly braces when importing.

// logger.js
export default function log(message) {
  console.log(`[LOG] ${message}`);
}
// main.js
import log from './logger.js';

log('Hello'); // [LOG] Hello

You can name default imports whatever you want:

import myLogger from './logger.js';

Mix default and named exports in the same file:

// utils.js
export default function helper() { }

export function format() { }
import helper, { format } from './utils.js';

Re-exporting

You can re-export things from one module to another without using them directly:

export { add } from './math.js';
export { default } from './logger.js';

This is useful for creating barrel files that expose a clean API:

// index.js - barrel file
export { add, subtract } from './math.js';
export { format } from './string.js';
export { log } from './logger.js';

Now users can import everything from one place:

import { add, format, log } from './index.js';

Dynamic Imports

Static import statements must be at the top of your file and run before the code executes. Sometimes you need to load modules conditionally.

Dynamic import() returns a promise:

button.addEventListener('click', async () => {
  const { format } = await import('./string.js');
  format('hello');
});

This is useful for code splitting—loading heavy modules only when needed. A browser will only download the module when the user clicks the button, not when the page loads.

The type=“module” Script Attribute

Browsers need to know when to treat a script as a module:

<script type="module" src="main.js"></script>

Module scripts are deferred automatically, meaning they don’t block HTML parsing. The browser downloads main.js but continues parsing HTML at the same time.

You can also use inline modules:

<script type="module">
  import { add } from './math.js';
  console.log(add(1, 2));
</script>

Common Pitfalls

Modules are scoped differently than regular scripts. Variables in a module aren’t global:

// script1.js
var count = 0; // this stays in this module

// script2.js  
var count = 0; // this is a different variable, not the same one

CORS blocks module scripts loaded from different origins unless the server sends proper headers:

// This fails without CORS headers
import { something } from 'https://other-domain.com/module.js';

The importing file needs the .js extension when working with local files in most bundlers. Node.js can sometimes infer it, but browsers need the exact path.

See Also