JavaScript Modules: How Import and Export Work

The JavaScript module system, officially known as ECMAScript Modules (ESM), provides a standardized mechanism for splitting code into separate, reusable files using the import and export statements. This article breaks down how this system operates, covering the syntax for named and default exports, the mechanics of static and dynamic imports, and the underlying three-phase lifecycle the browser or runtime uses to load and execute modular code.

Fundamentals of ES Modules

In the ES module system, each JavaScript file represents a distinct module. Code inside a module runs automatically in strict mode ("use strict"), and variables declared at the top level are scoped locally to that file rather than polluting the global scope. To share functionality—such as functions, objects, or primitive values—across files, you must explicitly export them from the source file and import them into the destination file.

Exporting Functionality

JavaScript provides two primary ways to expose module features: named exports and default exports.

1. Named Exports

Named exports allow a single module to export multiple values by their identifiers. You can declare them inline or group them at the bottom of the file.

// Inline named exports
export const apiKey = "XYZ123";
export function calculateTotal(a, b) {
  return a + b;
}

// Grouped named exports
const user = "Alex";
const role = "Admin";
export { user, role };

2. Default Exports

Each module can have at most one default export. This is commonly used when a file is designed to export a single primary entity, such as a class or component.

// Default export
export default function logger(message) {
  console.log(`[Log]: ${message}`);
}

Importing Functionality

The import statement retrieves bindings exported by other modules.

1. Importing Named Exports

Named imports must match the exact names of the exported identifiers and be wrapped in curly braces:

import { apiKey, calculateTotal } from './config.js';

You can also rename imports using the as keyword to prevent naming collisions:

import { calculateTotal as add } from './config.js';

2. Importing Default Exports

Default exports are imported without curly braces and can be assigned any local name:

import customLogger from './logger.js';

3. Namespace Imports

To import all named exports from a file as a single object, use the * as syntax:

import * as Config from './config.js';
console.log(Config.apiKey);

How the Module System Operates Under the Hood

The JavaScript engine executes modules in a three-phase process before any code runs:

  1. Construction (Parsing and Fetching): The engine resolves the file path, downloads the module file, and parses it into a data structure known as a Module Record. It statically scans for all import and export statements to build a complete dependency graph without running the code.
  2. Instantiation: The engine allocates memory spaces for all exported values and maps the imported bindings directly to those memory locations. Unlike older module patterns (such as CommonJS), ES Modules create live bindings. If a module updates an exported variable internally, the importing module immediately observes the updated value.
  3. Evaluation: The engine executes the module code from the bottom of the dependency tree upward, filling the allocated memory spaces with real values. Each module is evaluated only once; subsequent imports reference the cached result.

Static vs. Dynamic Imports

Standard import declarations are static. They must appear at the top level of a file and cannot be nested inside loops, conditionals, or functions. This rigidity enables build tools and engines to optimize code via dead-code elimination (tree shaking).

For use cases requiring on-demand loading, JavaScript provides the dynamic import() function. It returns a Promise that resolves to the requested module:

if (userIsAdmin) {
  import('./adminDashboard.js')
    .then((module) => {
      module.renderDashboard();
    })
    .catch((err) => {
      console.error("Failed to load module", err);
    });
}

Dynamic imports are ideal for code splitting, performance optimization, and conditional resource loading.