JavaScript Fundamentals: ES Modules: import and export

· 6 min read · Updated March 7, 2026 · beginner
modules import export es6 beginner

ES Modules let you split your JavaScript code across multiple files. Instead of dumping everything into one massive file, you can create small, focused modules that do one thing well and export the pieces other files need. This makes your code easier to organize, test, and maintain. In this tutorial, you’ll learn how to export from a module and import it elsewhere.

Why Use Modules?

Before ES Modules arrived in 2015, JavaScript had no built-in way to share code between files. Developers relied on workarounds like including multiple script tags in HTML, which created global variable pollution and made dependency management messy.

ES Modules solve these problems by giving each file its own scope. Variables and functions you declare in a module don’t accidentally leak into other files. You explicitly choose what to expose through exports, and other files explicitly request what they need through imports.

This approach also enables better tooling. Bundlers like webpack can analyze your imports and exports to remove unused code (tree-shaking), and browsers can load modules asynchronously for better performance.

The export Statement

You can export values from a module in two ways: named exports and a default export. A module can have multiple named exports but only one default export.

Named Exports

Named exports let you expose specific values from your module. You can export declarations directly, or export names that were declared elsewhere in the file.

// math.js - Export declarations directly
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export class Calculator {
  constructor(initialValue = 0) {
    this.value = initialValue;
  }
  
  add(n) {
    this.value += n;
    return this.value;
  }
}

You can also export a list of names declared elsewhere:

// utils.js
const formatDate = (date) => {
  return date.toISOString().split('T')[0];
};

const capitalize = (str) => {
  return str.charAt(0).toUpperCase() + str.slice(1);
};

// Export both at once
export { formatDate, capitalize };

You can rename exports to avoid conflicts or create cleaner public APIs:

// Renaming on export
export { formatDate as formatIsoDate, capitalize as titleCase };

Default Export

Each module can have one default export. The default export doesn’t have a fixed name, so the importing file gets to choose what to call it.

// logger.js - Default export
const log = (message) => {
  console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
};

export default log;

// You can also write it inline:
export default function(message) {
  console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
}

The difference matters when importing. Named exports must be imported with their exact names (or aliased), but default exports can be renamed to anything.

The import Statement

Importing lets you bring exported values into another module. There are several import forms to match different export patterns.

Named Imports

Use curly braces to import specific named exports:

// main.js
import { add, subtract, PI } from './math.js';

console.log(add(5, 3));      // 8
console.log(subtract(10, 4)); // 6
console.log(PI);             // 3.14159

You can rename imports to avoid conflicts:

import { add as sum, subtract as minus } from './math.js';

console.log(sum(2, 3)); // 5
console.log(minus(10, 3)); // 7

Default Import

Import a default export without curly braces:

import log from './logger.js';

log('Application started'); // [LOG] 2026-03-07T04:00:00.000Z: Application started

Since default exports don’t have a fixed name, you can call the import whatever you want:

import myLogger from './logger.js';
import whatever from './logger.js';
// Both work - you're naming it yourself

Namespace Import

Import everything as a single object:

import * as MathUtils from './math.js';

console.log(MathUtils.add(2, 3));       // 5
console.log(MathUtils.PI);              // 3.14159
console.log(MathUtils.Calculator);      // [class Calculator]

The namespace object contains all exports as properties. The default export is available as the default property:

import * as logger from './logger.js';
logger.default('Hello'); // Works - default export is on .default

Combining Imports

You can mix default and named imports in one statement:

import log, { formatDate, capitalize } from './utils.js';

log(capitalize(formatDate(new Date())));

Using Modules in the Browser

To use ES Modules in a browser, add type="module" to your script tag:

<!-- index.html -->
<script type="module" src="main.js"></script>

<!-- You can also write inline modules -->
<script type="module">
  import { add } from './math.js';
  console.log(add(2, 3)); // 5
</script>

When you use modules in the browser, you need to run them through a local web server. Opening the HTML file directly in the browser (file:// protocol) won’t work due to CORS restrictions. If you’re using VS Code, the Live Server extension handles this nicely.

# Using Python's built-in server
python -m http.server 8000

# Or Node's http-server
npx http-server -p 8000

File Extensions

You might see modules with .mjs extension instead of .js. The .mjs extension makes it clear which files are modules, and Node.js treats them as ES modules by default. Both work in browsers:

// This works
import { add } from './math.js';

// So does this
import { add } from './math.mjs';

For a beginner, sticking with .js is fine, but know that .mjs exists if you encounter it.

Common Patterns and Gotchas

Importing from Packages

When importing from npm packages, use the package name directly:

import { format } from 'date-fns';
import chalk from 'chalk';

console.log(format(new Date(), 'yyyy-MM-dd'));
console.log(chalk.green('Success!'));

Modules Are Singletons

A module is executed only once. Subsequent imports get a reference to the same exported values. This is useful for sharing state:

// counter.js
export let count = 0;
export function increment() {
  count++;
}

// main.js
import { count, increment } from './counter.js';
import { count as count2, increment as inc2 } from './counter.js';

increment();
console.log(count);  // 1
console.log(count2); // 1 - same value!

Live Bindings

Exported values are live bindings to the original. If the exporting module changes a value, importers see the change:

// state.js
export let value = 'initial';

setTimeout(() => {
  value = 'changed';
}, 1000);

// main.js
import { value } from './state.js';

console.log(value); // "initial"
setTimeout(() => {
  console.log(value); // "changed" - updated from the other module!
}, 2000);

Re-exporting

You can re-export values from other modules, which is useful for creating “barrel” files that aggregate exports:

// index.js - Barrel file
export { add, subtract } from './math.js';
export { formatDate, capitalize } from './utils.js';
export { default as log } from './logger.js';

// Now consumers can import from one file
import { add, formatDate, log } from './index.js';

Summary

ES Modules give JavaScript a standard way to organize code across files. You export what you want to share using export, and import it elsewhere with import. Named exports work well for multiple related values, while default exports are perfect for a module’s main functionality.

In the browser, use type="module" on your script tags and serve files through a local web server. Remember that modules are singletons with live bindings, which affects how you think about shared state.

These fundamentals prepare you for working with npm packages, modern frameworks like React and Vue, and build tools like webpack and Vite. Your next step is to try building a small project with multiple module files to solidify these concepts.