|

JavaScript 44 🧬 ES Modules — import/export

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

export const PI = 3.14159;

export default function multiply(a, b) {
  return a * b;
}

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

console.log(add(2, 3));
console.log(PI);
console.log(multiply(2, 3));

// Renaming imports
import { add as sum } from './math.js';
console.log(sum(1, 2));

// Namespace import
import * as math from './math.js';
console.log(math.add(1, 2));
console.log(math.PI);
console.log(math.default(2, 3));

// Dynamic import
const module = await import('./math.js');
console.log(module.add(1, 2));

ES Modules (ESM) are the official, standardized way to split JavaScript code into reusable files. They use import and export to share code between files, and they’ve replaced the older CommonJS (require/module.exports) approach in modern JavaScript.

Key point: ES Modules are static — imports are resolved at load time, not runtime (except for dynamic import()). Each module has its own scope, exports are live bindings, and this is undefined at the top level.


a – What are ES Modules

A module is a JavaScript file that has its own scope and can export values for other files to import. Before modules, everything ran in the global scope — leading to naming conflicts, load-order problems, and fragile code.

Why modules matter:

ProblemModule solution
Global namespace pollutionEach module has its own scope
Load order mattersImports are resolved automatically
ReusabilityExport once, import anywhere
MaintainabilitySplit code into focused files
Tree shakingBundlers drop unused exports
Explicit dependenciesImports show what a module needs

Enabling ES Modules:

In the browser:

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

In Node.js:

  • Use .mjs extension, or
  • Set "type": "module" in package.json, or
  • Run with node --input-type=module

ES Modules vs CommonJS:

FeatureESMCommonJS
Syntaximport / exportrequire / module.exports
LoadingStaticDynamic
AsyncYes (import())Sync
this at topundefinedmodule.exports
Tree shakingYesNo
Browser supportNativeNeeds bundler
Node.js.mjs or "type": "module"Default

CommonJS example (for comparison):

// math.js (CommonJS)
const add = (a, b) => a + b;
module.exports = { add };

// main.js (CommonJS)
const { add } = require('./math');
console.log(add(1, 2));

ES Modules example:

// math.js (ESM)
export const add = (a, b) => a + b;

// main.js (ESM)
import { add } from './math.js';
console.log(add(1, 2));

Module scope:

// a.js
const secret = 'a';

// b.js
const secret = 'b';   // no conflict — different modules

// main.js
import './a.js';
import './b.js';
// Both loaded, no global collision

Variables in modules don’t leak to the global scope.

Modules are singletons:

A module is evaluated once, no matter how many times it’s imported. All importers share the same instance.

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

// a.js
import { increment } from './counter.js';
increment();  // 1

// b.js
import { increment } from './counter.js';
increment();  // 2 — same instance

Modules are deferred:

Module scripts execute after the HTML is parsed, like defer. They don’t block page rendering.

<script type="module" src="main.js"></script>
<!-- Runs after DOM is ready -->

Strict mode by default:

All ES Modules run in strict mode — no need for 'use strict'. Assigning to an undeclared variable throws.

// In a module
x = 10;   // ReferenceError: x is not defined

Top-level this is undefined:

// In a module
console.log(this);
// [ undefined ]

// In a regular script
console.log(this);
// [ window ] (browser) or [ {} ] (Node.js)

Module scope vs global:

┌──────────────────────────────────────────────┐
│  Classic scripts                             │
│                                              │
│  <script>                                    │
│    var x = 1;   → window.x = 1              │
│  </script>                                   │
│  <script>                                    │
│    console.log(x);   → 1                    │
│  </script>                                   │
│                                              │
│  Shared global scope                         │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  ES modules                                  │
│                                              │
│  a.js                                        │
│    const x = 1;                              │
│                                              │
│  b.js                                        │
│    console.log(x);   → ReferenceError       │
│                                              │
│  Each module isolated                        │
│                                              │
└──────────────────────────────────────────────┘

b – Export and import syntax

ES Modules have several forms of export and import. This section covers them all.

Named exports:

Export individual values with names:

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

Or export at the end:

// math.js
const PI = 3.14159;
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }

export { PI, add, subtract };

Default export:

Each module can have one default export:

// greet.js
export default function greet(name) {
  return `Hello, ${name}`;
}

Default exports can be any value — function, class, object, primitive:

export default 42;
export default 'hello';
export default { a: 1 };
export default class Person {}

Mixing default and named exports:

// utils.js
export default function main() { return 'main'; }
export const helper = () => 'helper';
export const VERSION = '1.0';

Importing named exports:

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

console.log(add(1, 2));
console.log(PI);

Importing default exports:

// main.js
import greet from './greet.js';

console.log(greet('Alice'));

You can name the default whatever you want — the name isn’t fixed.

Importing both:

// main.js
import main, { helper, VERSION } from './utils.js';

console.log(main());
console.log(helper());
console.log(VERSION);

The default comes first, named imports in braces.

Renaming imports:

import { add as sum, PI as pi } from './math.js';

console.log(sum(1, 2));
console.log(pi);

Renaming exports:

// math.js
const add = (a, b) => a + b;
export { add as sum };

Namespace import:

Import everything as an object:

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

console.log(math.add(1, 2));
console.log(math.PI);
console.log(math.subtract(5, 3));

If there’s also a default export, it’s on .default:

import * as utils from './utils.js';

console.log(utils.default());
console.log(utils.helper());
console.log(utils.VERSION);

Side-effect only import:

Import a module for its side effects — no bindings:

import './styles.css';
import './polyfill.js';

The module runs but nothing is imported.

Re-exporting:

Forward exports from another module:

// index.js
export { add, subtract } from './math.js';
export { default as greet } from './greet.js';

This is common for barrel files — a single entry point for a folder.

Re-export everything:

// index.js
export * from './math.js';
export * from './string-utils.js';

Note: export * does not re-export defaults.

Import all + re-export:

// index.js
export * from './math.js';
export { default } from './main-utils.js';

All import/export forms:

FormMeaning
import x from './m.js'Default import
import { a, b } from './m.js'Named imports
import { a as x } from './m.js'Rename
import x, { a } from './m.js'Default + named
import * as m from './m.js'Namespace
import './m.js'Side effect only
export const x = 1Inline named export
export { x }Export at end
export { x as y }Rename export
export default xDefault export
export { x } from './m.js'Re-export
export * from './m.js'Re-export all
export * as m from './m.js'Re-export as namespace

Import paths:

Relative paths must start with ./ or ../:

import { add } from './math.js';      // ✅ same folder
import { add } from '../utils/math.js'; // ✅ parent folder
import { add } from 'math.js';         // ❌ looks in node_modules

Node.js and bundlers resolve bare imports from node_modules:

import React from 'react';
import { useState } from 'react';

Browsers can’t do bare imports without an import map:

<script type="importmap">
{
  "imports": {
    "react": "/node_modules/react/index.js"
  }
}
</script>

File extensions:

In browsers, extensions are required:

import { add } from './math.js';   // ✅
import { add } from './math';      // ❌ browser fails

Node.js requires the extension for relative imports (unless using a bundler).

A complete example:

// string-utils.js
export const upper = s => s.toUpperCase();
export const lower = s => s.toLowerCase();

export default function capitalize(s) {
  return s.charAt(0).toUpperCase() + s.slice(1);
}

// math.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;

// index.js — barrel file
export * from './string-utils.js';
export * from './math.js';
export { default as capitalize } from './string-utils.js';

// app.js
import { add, multiply, upper, lower, capitalize } from './index.js';

console.log(add(1, 2));          // 3
console.log(multiply(3, 4));     // 12
console.log(upper('hello'));     // HELLO
console.log(capitalize('hi'));   // Hi

A note on live bindings:

ES Module imports are live bindings — they reflect changes to the exported value:

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

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

console.log(count);   // 0
increment();
console.log(count);   // 1  ← live binding

You can’t reassign an imported binding, but you can see changes if the exporter updates it.


c – Dynamic imports and module patterns

Static imports are resolved before the module runs. Dynamic imports load modules at runtime — perfect for code splitting, conditional loading, and lazy evaluation.

Dynamic import():

import() is a function that returns a Promise resolving to the module namespace:

const module = await import('./math.js');
console.log(module.add(1, 2));

// Or with .then()
import('./math.js').then(m => {
  console.log(m.add(1, 2));
});

Key differences from static import:

Aspectimportimport()
When resolvedLoad timeRuntime
ReturnsModule bindingPromise
Where allowedTop levelAnywhere
Named bindingsDirectOn module object
Conditional

Conditional loading:

if (user.isAdmin) {
  const { adminPanel } = await import('./admin.js');
  adminPanel.open();
}

Lazy loading on demand:

button.addEventListener('click', async () => {
  const { openModal } = await import('./modal.js');
  openModal();
});

The modal code is only downloaded when the user clicks.

Code splitting in bundlers:

const { default: HeavyComponent } = await import('./HeavyComponent.js');
render(HeavyComponent);

Bundlers like Vite and Webpack create a separate chunk for HeavyComponent.js, loaded only when needed.

Error handling with dynamic imports:

try {
  const module = await import('./maybe-missing.js');
  module.run();
} catch (err) {
  console.error('Failed to load module:', err.message);
}

If the module fails to load (network error, syntax error), the Promise rejects.

Dynamic import with named exports:

const { add, subtract } = await import('./math.js');
console.log(add(1, 2));
console.log(subtract(5, 3));

Destructure named exports directly from the module object.

Dynamic import with default:

const { default: greet } = await import('./greet.js');
console.log(greet('Alice'));

You need to rename default because it’s a reserved word.

Import maps for browser module resolution:

<script type="importmap">
{
  "imports": {
    "lodash": "/vendor/lodash-es.js",
    "utils/": "/js/utils/"
  }
}
</script>

<script type="module">
  import { chunk } from 'lodash';
  import { add } from 'utils/math.js';
</script>

Import maps let you use bare specifiers in browsers.

Module patterns:

Pattern 1 — Barrel file (index.js):

// components/index.js
export { Button } from './Button.js';
export { Input } from './Input.js';
export { Modal } from './Modal.js';

// consumer
import { Button, Input, Modal } from './components/index.js';

Consumers import from one entry point, not individual files.

Pattern 2 — Facade module:

// api/index.js
export { get as getUser } from './user.js';
export { get as getOrder } from './order.js';
export { post } from './http.js';

Pattern 3 — Conditional exports in package.json:

{
  "exports": {
    ".": {
      "import": "./index.mjs",
      "require": "./index.cjs"
    }
  }
}

Pattern 4 — Circular dependency awareness:

Circular imports are possible but tricky. Keep modules acyclic when you can.

// a.js
import { b } from './b.js';
export const a = () => b();

// b.js
import { a } from './a.js';
export const b = () => 'b';

The above works because b doesn’t call a at module load time — only inside the function.

Pattern 5 — Lazy module initialization:

let instance = null;

export async function getInstance() {
  if (!instance) {
    const { Heavy } = await import('./heavy.js');
    instance = new Heavy();
  }
  return instance;
}

Pattern 6 — Polyfills via side effect:

// main.js
import './polyfills.js';
import { run } from './app.js';

run();

polyfills.js runs when imported but exports nothing.

Pattern 7 — Environment-specific modules:

const config = await import(`./config.${process.env.NODE_ENV}.js`);
export default config.default;

Pattern 8 — Route-based code splitting:

const routes = {
  '/': () => import('./pages/Home.js'),
  '/about': () => import('./pages/About.js'),
  '/contact': () => import('./pages/Contact.js')
};

async function navigate(path) {
  const { default: Page } = await routes[path]();
  render(Page);
}

Pattern 9 — Feature flags:

if (featureFlags.newDashboard) {
  const { Dashboard } = await import('./new-dashboard.js');
  render(Dashboard);
} else {
  const { Dashboard } = await import('./old-dashboard.js');
  render(Dashboard);
}

Pattern 10 — Testing with dynamic imports:

const { sum } = await import('./utils.js');
test('sum adds numbers', () => {
  expect(sum(1, 2)).toBe(3);
});

Module loading order:

┌──────────────────────────────────────────────┐
│  Static imports                              │
│                                              │
│  main.js                                     │
│    import { a } from './a.js';               │
│    import { b } from './b.js';               │
│                                              │
│  1. Fetch a.js                               │
│  2. Fetch b.js                               │
│  3. Evaluate a.js                            │
│  4. Evaluate b.js                            │
│  5. Evaluate main.js                         │
│                                              │
│  All modules loaded before main.js runs      │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Dynamic imports                             │
│                                              │
│  main.js                                     │
│    button.onclick = async () => {            │
│      const m = await import('./modal.js');   │
│    };                                        │
│                                              │
│  1. main.js runs                             │
│  2. User clicks button                       │
│  3. Fetch modal.js                           │
│  4. Evaluate modal.js                        │
│  5. Result returned                          │
│                                              │
│  Loaded on demand                            │
│                                              │
└──────────────────────────────────────────────┘

Complete Example Session

// ============================================
// PART 1: NAMED EXPORTS
// ============================================

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

// main.js
import { add, subtract, PI } from './math.js';
console.log(add(1, 2));
// [ 3 ]
console.log(PI);
// [ 3.14159 ]

// ============================================
// PART 2: DEFAULT EXPORT
// ============================================

// greet.js
export default function greet(name) {
  return `Hello, ${name}`;
}

// main.js
import greet from './greet.js';
console.log(greet('Alice'));
// [ Hello, Alice ]

// ============================================
// PART 3: MIXED EXPORTS
// ============================================

// utils.js
export default function main() { return 'main'; }
export const helper = () => 'helper';
export const VERSION = '1.0';

// main.js
import main, { helper, VERSION } from './utils.js';
console.log(main());
// [ main ]
console.log(helper());
// [ helper ]
console.log(VERSION);
// [ 1.0 ]

// ============================================
// PART 4: RENAMING
// ============================================

// main.js
import { add as sum } from './math.js';
console.log(sum(1, 2));
// [ 3 ]

// ============================================
// PART 5: NAMESPACE IMPORT
// ============================================

// main.js
import * as math from './math.js';
console.log(math.add(1, 2));
// [ 3 ]
console.log(math.PI);
// [ 3.14159 ]

// ============================================
// PART 6: SIDE EFFECT ONLY
// ============================================

// main.js
import './polyfill.js';
// polyfill.js runs but nothing imported

// ============================================
// PART 7: RE-EXPORT
// ============================================

// index.js
export { add, subtract } from './math.js';
export { default as greet } from './greet.js';

// main.js
import { add, greet } from './index.js';
console.log(add(1, 2));
// [ 3 ]
console.log(greet('Alice'));
// [ Hello, Alice ]

// ============================================
// PART 8: EXPORT ALL
// ============================================

// index.js
export * from './math.js';
export * from './string-utils.js';

// main.js
import { add, upper } from './index.js';

// ============================================
// PART 9: DYNAMIC IMPORT
// ============================================

async function load() {
  const module = await import('./math.js');
  console.log(module.add(1, 2));
}
load();
// [ 3 ]

// ============================================
// PART 10: CONDITIONAL DYNAMIC
// ============================================

async function init(user) {
  if (user.isAdmin) {
    const { adminPanel } = await import('./admin.js');
    adminPanel.open();
  }
}

// ============================================
// PART 11: LAZY LOADING
// ============================================

document.addEventListener('DOMContentLoaded', () => {
  const button = document.querySelector('#show-modal');
  button.addEventListener('click', async () => {
    const { openModal } = await import('./modal.js');
    openModal();
  });
});

// ============================================
// PART 12: DESTRUCTURING DYNAMIC IMPORTS
// ============================================

async function run() {
  const { add, subtract } = await import('./math.js');
  console.log(add(1, 2));
  console.log(subtract(5, 3));
}
run();
// [ 3 ]
// [ 2 ]

// ============================================
// PART 13: DYNAMIC DEFAULT
// ============================================

async function greet() {
  const { default: greetFn } = await import('./greet.js');
  console.log(greetFn('Alice'));
}
greet();
// [ Hello, Alice ]

// ============================================
// PART 14: LIVE BINDINGS
// ============================================

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

// main.js
import { count, increment } from './counter.js';
console.log(count);
// [ 0 ]
increment();
console.log(count);
// [ 1 ]

// ============================================
// PART 15: MODULE SINGLETON
// ============================================

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

// a.js
import { increment } from './counter.js';
increment();
// [ 1 ]

// b.js
import { increment } from './counter.js';
increment();
// [ 2 ]

// ============================================
// PART 16: BARREL FILE
// ============================================

// components/index.js
export { Button } from './Button.js';
export { Input } from './Input.js';

// app.js
import { Button, Input } from './components/index.js';

// ============================================
// PART 17: DYNAMIC WITH ERROR HANDLING
// ============================================

async function safeImport(path) {
  try {
    return await import(path);
  } catch (err) {
    console.error('Failed:', err.message);
    return null;
  }
}

// ============================================
// PART 18: ROUTE-BASED SPLITTING
// ============================================

const routes = {
  '/': () => import('./pages/Home.js'),
  '/about': () => import('./pages/About.js')
};

async function navigate(path) {
  const { default: Page } = await routes[path]();
  render(Page);
}

// ============================================
// PART 19: ENVIRONMENT-SPECIFIC
// ============================================

// const config = await import(`./config.${process.env.NODE_ENV}.js`);
// export default config.default;

// ============================================
// PART 20: FULL SCRIPT
// ============================================

// math44.js
export function add44(a, b) {
  return a + b;
}

export const PI44 = 3.14159;

export default function multiply44(a, b) {
  return a * b;
}

// main44.js
import multiply44, { add44, PI44 } from './math44.js';

console.log(add44(2, 3));
console.log(PI44);
console.log(multiply44(2, 3));

import { add44 as sum44 } from './math44.js';
console.log(sum44(1, 2));

import * as math44 from './math44.js';
console.log(math44.add44(1, 2));
console.log(math44.PI44);
console.log(math44.default(2, 3));

const module44 = await import('./math44.js');
console.log(module44.add44(1, 2));

Quick Reference

Export Forms

FormExample
Named exportexport const x = 1;
Named export (end)export { x, y };
Renameexport { x as y };
Defaultexport default function() {}
Re-exportexport { x } from './m.js';
Re-export allexport * from './m.js';
Namespace re-exportexport * as ns from './m.js';

Import Forms

FormExample
Namedimport { x } from './m.js';
Renameimport { x as y } from './m.js';
Defaultimport x from './m.js';
Default + namedimport x, { y } from './m.js';
Namespaceimport * as m from './m.js';
Side-effectimport './m.js';
Dynamicawait import('./m.js')

Export Types

TypeCountImport style
NamedManyimport { x }
DefaultOneimport x
Both1 + manyimport x, { y }

ESM vs CommonJS

AspectESMCommonJS
Syntaximport/exportrequire/module.exports
LoadingStaticDynamic
AsyncYesNo
this at topundefinedmodule.exports
Strict modeAlwaysOptional
Tree shakingYesNo
BrowserNativeBundler

Static vs Dynamic

Aspectimportimport()
TimingLoadRuntime
ReturnsBindingPromise
Condition
LocationTop levelAnywhere
SplittingNoYes

Paths

PathMeaning
./m.jsSame folder
../m.jsParent folder
/m.jsAbsolute (web root)
pkgnode_modules (Node/bundler)
https://...Remote (browser)

Key Rules

RuleDetails
Module scopeVariables not global
SingletonEvaluated once
DeferredRuns after HTML parsed
Strict modeAlways
Top-level thisundefined
ExtensionsRequired in browser
Relative pathsMust start with ./ or ../

Best Practices

Do This:

// Use named exports for most things
export function add(a, b) { return a + b; }    // ✅

// Use default for the main thing a module provides
export default class User {}                   // ✅

// Use explicit extensions in browser code
import { x } from './m.js';                    // ✅

// Use barrel files for folders
export * from './Button.js';                   // ✅

// Use dynamic imports for code splitting
const m = await import('./heavy.js');          // ✅

// Prefer static imports when possible
import { x } from './m.js';                    // ✅

// Use namespaces to avoid conflicts
import * as math from './math.js';             // ✅

// Re-export to create clean public APIs
export { add } from './math.js';               // ✅

// Use dynamic imports with try/catch
try { await import('./m.js'); } catch (e) {}   // ✅

Don’t Do This:

// Don't use extensionless paths in browsers
import { x } from './m';                       // ❌ browser fails

// Don't use bare paths without import maps
import { x } from 'math';                      // ❌ browser fails
import { x } from 'math';                      // ✅ in Node/bundler

// Don't mix require and import
const x = require('./m');                      // ❌ in ESM
import { x } from './m.js';                    // ✅

// Don't use module.exports in ESM
module.exports = { x };                        // ❌ in ESM

// Don't reassign imported bindings
import { x } from './m.js';
x = 10;                                        // ❌ TypeError

// Don't rely on load order for side effects
import './a.js';                               // ⚠️  race conditions

// Don't create circular dependencies
// a.js imports b.js, b.js imports a.js         // ⚠️  fragile

// Don't use dynamic imports for everything
const m = await import('./small.js');          // ⚠️  overhead

Common Pitfalls

PitfallProblemSolution
Missing ./Bare import failsAdd ./
Missing extensionBrowser failsAdd .js
Mixing require/importErrorUse ESM throughout
Reassigning importsTypeErrorUse getters/exports
Circular depsUndefined valuesRefactor
this at topundefinedDon’t rely on it
__dirname missingReferenceErrorUse import.meta.url
Dynamic import pathNot a literalUse static or import.meta

import.meta in ESM:

console.log(import.meta.url);
// [ file:///path/to/module.js ]

// Get directory
const dir = new URL('.', import.meta.url).pathname;

Replaces __dirname in CommonJS.


Real-World Examples

1. Named Exports

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

// main.js
import { add, PI } from './math.js';
console.log(add(1, 2));
// [ 3 ]

2. Default Export

// greet.js
export default function greet(name) {
  return `Hello, ${name}`;
}

// main.js
import greet from './greet.js';
console.log(greet('Alice'));
// [ Hello, Alice ]

3. Mixed Exports

// utils.js
export default function main() { return 'main'; }
export const helper = () => 'helper';

// main.js
import main, { helper } from './utils.js';

4. Renaming

import { add as sum } from './math.js';
console.log(sum(1, 2));
// [ 3 ]

5. Namespace Import

import * as math from './math.js';
console.log(math.add(1, 2));
console.log(math.PI);

6. Side-Effect Import

import './polyfill.js';

7. Re-Export

// index.js
export { add, subtract } from './math.js';
export { default as greet } from './greet.js';

8. Export All

// index.js
export * from './math.js';
export * from './string-utils.js';

9. Dynamic Import

const module = await import('./math.js');
console.log(module.add(1, 2));
// [ 3 ]

10. Conditional Dynamic Import

if (user.isAdmin) {
  const { adminPanel } = await import('./admin.js');
  adminPanel.open();
}

11. Lazy Loading

button.addEventListener('click', async () => {
  const { openModal } = await import('./modal.js');
  openModal();
});

12. Destructured Dynamic Import

const { add, subtract } = await import('./math.js');

13. Dynamic Default Import

const { default: greet } = await import('./greet.js');
console.log(greet('Alice'));

14. Live Bindings

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

// main.js
import { count, increment } from './counter.js';
increment();
console.log(count);
// [ 1 ]

15. Module Singleton

// singleton.js
let instance;
export function getInstance() {
  return instance ||= {};
}

16. Barrel File

// components/index.js
export { Button } from './Button.js';
export { Input } from './Input.js';

17. Dynamic with Error Handling

try {
  const m = await import('./maybe.js');
} catch (err) {
  console.error('Failed:', err.message);
}

18. Route-Based Splitting

const routes = {
  '/': () => import('./pages/Home.js'),
  '/about': () => import('./pages/About.js')
};

19. Environment Config

const config = await import(`./config.${process.env.NODE_ENV}.js`);
export default config.default;

20. Full Script

// math44.js
export function add44(a, b) {
  return a + b;
}

export const PI44 = 3.14159;

export default function multiply44(a, b) {
  return a * b;
}

// main44.js
import multiply44, { add44, PI44 } from './math44.js';

console.log(add44(2, 3));
console.log(PI44);
console.log(multiply44(2, 3));

import { add44 as sum44 } from './math44.js';
console.log(sum44(1, 2));

import * as math44 from './math44.js';
console.log(math44.add44(1, 2));
console.log(math44.PI44);
console.log(math44.default(2, 3));

const module44 = await import('./math44.js');
console.log(module44.add44(1, 2));

Visual: Module Loading

┌──────────────────────────────────────────────┐
│  main.js                                     │
│  import { a } from './a.js'                  │
│  import { b } from './b.js'                  │
│                                              │
│  a.js                                        │
│  import { c } from './c.js'                  │
│                                              │
│  Dependency graph:                           │
│                                              │
│  main.js                                     │
│   ├── a.js                                   │
│   │    └── c.js                              │
│   └── b.js                                   │
│                                              │
│  Evaluation order (depth-first):             │
│  c.js → a.js → b.js → main.js                │
│                                              │
└──────────────────────────────────────────────┘

Visual: Static vs Dynamic

┌──────────────────────────────────────────────┐
│  Static import                               │
│                                              │
│  import { x } from './m.js'                  │
│                                              │
│  • Resolved before code runs                 │
│  • Named bindings available                  │
│  • Can be tree-shaken                        │
│  • Blocking (in dependency order)            │
│  • Top level only                            │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Dynamic import                              │
│                                              │
│  const m = await import('./m.js')            │
│                                              │
│  • Resolved at runtime                       │
│  • Returns a Promise                         │
│  • Always loads whole module                 │
│  • Non-blocking                              │
│  • Anywhere in code                          │
│  • Enables code splitting                    │
│                                              │
└──────────────────────────────────────────────┘

Visual: Export/Import Types

┌──────────────────────────────────────────────┐
│  math.js                                     │
│                                              │
│  export const PI = 3.14;      ← named        │
│  export function add() {}      ← named       │
│  export default function() {}  ← default     │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  main.js                                     │
│                                              │
│  import def from './math.js'                 │
│    → gets the default                        │
│                                              │
│  import { PI, add } from './math.js'         │
│    → gets named                              │
│                                              │
│  import def, { PI } from './math.js'         │
│    → gets both                               │
│                                              │
│  import * as math from './math.js'           │
│    → namespace object                        │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptSyntaxExample
Named exportexport const x = 1export function add() {}
Default exportexport default xexport default function() {}
Named importimport { x } from './m.js'Get named binding
Default importimport x from './m.js'Get default
Mixedimport x, { y } from './m.js'Both
Renameimport { x as y }Rename import
Namespaceimport * as mAll as object
Side effectimport './m.js'Just run
Re-exportexport { x } from './m.js'Forward
Export allexport * from './m.js'Forward all
Dynamicawait import('./m.js')Runtime load
Dynamic defaultconst { default: x } = await import(...)Destructure

Key takeaways:

  • ES Modules are the standard for splitting JavaScript into files
  • Use export to expose values; import to consume them
  • Named exports allow many per module; default allows one
  • Namespace import (import * as m) groups everything into an object
  • Re-exports and barrel files create clean public APIs
  • Dynamic imports (import()) load at runtime and return a Promise
  • Modules have their own scope — no global leakage
  • Modules are singletons — evaluated once, shared everywhere
  • Modules run in strict mode by default — no 'use strict' needed
  • this at the top level is undefined
  • import.meta.url replaces __dirname in ESM
  • File extensions are required for relative imports in browsers
  • Use ./ or ../ for relative paths — bare paths are for node_modules

Remember: ES Modules are how modern JavaScript is organized. Export what you want to share, import what you need. Use named exports for flexibility, default for the main thing. Reach for dynamic import() when you need conditional loading or code splitting. Keep your modules focused and avoid circular dependencies. Master ESM, and your code becomes organized, tree-shakeable, and ready for the modern web.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!