JavaScript ES6 Modules Import Export Guide

A complete guide to JavaScript ES6 modules. Learn the import and export syntax, how modules differ from scripts, named vs default exports, re-exporting, module scope and strict mode, browser module support with type=module, and organizing large codebases with ES modules.

JavaScriptintermediate
13 min read

ES6 modules are the native module system for JavaScript. They let you split code across files, declare public APIs with export, and consume them with import. Modules run in strict mode, have their own scope, and are statically analyzable, enabling tree-shaking and other build optimizations.

Modules vs Scripts

FeatureScript (<script>)Module (<script type="module">)
ScopeGlobal (shared window)Module-level (isolated)
Strict modeOptionalAlways strict
Top-level thiswindowundefined
import / exportNot availableAvailable
LoadingSynchronous by defaultDeferred by default
CORSNot required for same-originRequired for cross-origin

Named Exports

Named exports make specific bindings available by name:

javascriptjavascript
// math.js
export const PI = 3.14159;
 
export function add(a, b) {
  return a + b;
}
 
export function multiply(a, b) {
  return a * b;
}

Exporting at the Bottom

You can also declare first and export at the end:

javascriptjavascript
// math.js
const PI = 3.14159;
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
 
export { PI, add, multiply };

Both approaches create identical named exports. The bottom-export style is useful when you want a clear "public API" section. See JavaScript named exports a complete tutorial for a deep dive.

Default Exports

Each module can have at most one default export:

javascriptjavascript
// Logger.js
export default class Logger {
  constructor(prefix) {
    this.prefix = prefix;
  }
 
  log(message) {
    console.log(`[${this.prefix}] ${message}`);
  }
}

See JavaScript default exports complete tutorial for the full guide on defaults.

Importing Named Exports

javascriptjavascript
// Import specific named exports
import { add, multiply } from "./math.js";
 
console.log(add(2, 3)); // 5
 
// Import with alias (rename)
import { add as sum, PI } from "./math.js";
console.log(sum(2, 3)); // 5
console.log(PI);         // 3.14159

Import All as Namespace

javascriptjavascript
import * as math from "./math.js";
 
console.log(math.PI);          // 3.14159
console.log(math.add(2, 3));   // 5
console.log(math.multiply(4, 5)); // 20

Importing Default Exports

javascriptjavascript
// No curly braces — you choose the local name
import Logger from "./Logger.js";
import MyLogger from "./Logger.js"; // same thing, different name
 
const log = new Logger("app");
log.log("started"); // [app] started

Importing Default and Named Together

javascriptjavascript
// utils.js
export default function main() { /* ... */ }
export function helper1() { /* ... */ }
export function helper2() { /* ... */ }
 
// consumer.js
import main, { helper1, helper2 } from "./utils.js";

Re-Exporting

Barrel files aggregate and re-export from multiple modules:

javascriptjavascript
// components/index.js
export { Button } from "./Button.js";
export { Modal } from "./Modal.js";
export { Tooltip } from "./Tooltip.js";
 
// Re-export a default as a named export
export { default as Header } from "./Header.js";
 
// Re-export everything from a module
export * from "./utils.js";

Consumers import from the barrel:

javascriptjavascript
import { Button, Modal, Header } from "./components/index.js";

Renaming Exports

javascriptjavascript
// internal.js
function internalName() { /* ... */ }
 
export { internalName as publicName };
javascriptjavascript
// consumer.js
import { publicName } from "./internal.js";

This is useful when a module's internal name differs from the API name you want to expose. For the destructuring equivalent, see renaming variables during JS destructuring guide.

Module Scope

Each module has its own top-level scope. Variables declared in a module are not global:

javascriptjavascript
// counter.js
let count = 0;
 
export function increment() {
  count++;
}
 
export function getCount() {
  return count;
}
javascriptjavascript
// main.js
import { increment, getCount } from "./counter.js";
 
increment();
increment();
console.log(getCount()); // 2
 
// count is not accessible here — it's private to counter.js

Modules are singletons -- the module body runs once, and all importers share the same instance. count above is shared state.

Browser Usage

htmlhtml
<!-- type="module" enables ES module syntax -->
<script type="module" src="./main.js"></script>
 
<!-- Inline module -->
<script type="module">
  import { add } from "./math.js";
  console.log(add(1, 2));
</script>
 
<!-- Fallback for browsers without module support -->
<script nomodule src="./bundle-legacy.js"></script>

Module scripts are deferred automatically (no need for defer attribute) and execute after the DOM is parsed.

Common Patterns

Conditional Exports Object

javascriptjavascript
// config.js
const dev  = { apiUrl: "http://localhost:3000", debug: true };
const prod = { apiUrl: "https://api.example.com", debug: false };
 
export default process.env.NODE_ENV === "production" ? prod : dev;

Exporting Constants and Enums

javascriptjavascript
// status.js
export const Status = Object.freeze({
  PENDING:  "pending",
  ACTIVE:   "active",
  INACTIVE: "inactive",
});
Rune AI

Rune AI

Key Insights

  • Modules have isolated scope: Variables in a module are not global; only explicitly exported bindings are accessible to importers
  • Named exports use curly braces on import: import { name } from "./mod.js" -- the braces are not destructuring, they are module import syntax
  • Default exports allow any local name on import: import Whatever from "./mod.js" -- the name is your choice
  • Modules are singletons: The module body executes once; all importers share the same live bindings and state
  • Static imports enable tree-shaking: Bundlers can analyze the import graph at build time and eliminate unused exports from the final bundle
RunePowered by Rune AI

Frequently Asked Questions

Can I use import inside a function or if block?

No. Static `import` declarations must be at the top level of a module. For conditional loading, use dynamic `import()`, which returns a Promise. See [dynamic imports in JavaScript complete guide](/tutorials/programming-languages/javascript/dynamic-imports-in-javascript-complete-guide).

Do circular imports cause errors?

Not necessarily. ES modules handle circular dependencies through live bindings. However, if you access a binding before the exporting module has initialized it, you get `undefined`. Design modules to avoid circular dependencies when possible.

Are ES modules synchronous or asynchronous?

Module loading is asynchronous (the browser fetches them over the network), but once loaded, the module graph is linked and evaluated in a deterministic order. The `import` declaration itself is not a runtime async operation.

Should I always include the .js extension in import paths?

In browsers and Node.js with ESM, yes. Bundlers like webpack and Vite can resolve extensionless paths, but native ESM requires the full specifier including the extension.

What is the difference between export default and export as default?

`export default expr` exports the expression as the default. `export { name as default }` re-exports an existing named binding as the default. They produce the same result but serve different use cases.

Conclusion

ES6 modules provide a clean, standardized way to organize JavaScript code. Named exports give precise, tree-shakable imports. Default exports provide a convenient single-value entry point. Re-exports enable barrel patterns for large codebases. Modules run in strict mode with isolated scope, preventing global pollution. For deeper coverage of each export style, see JavaScript named exports a complete tutorial and JavaScript default exports complete tutorial.