How do you import and export modules in JavaScript?
TL;DR
ECMAScript modules use export to define a module's public bindings and import to consume them. Named imports must match exported names; a module can also have one default export whose importer chooses the local name. Static imports are resolved before the module executes and are best for normal dependencies. Use import() when a dependency is genuinely conditional or should be loaded on demand.
Resolution depends on the host. Browsers need module scripts and URL-like specifiers (unless an import map is used); Node.js uses its ESM and package-resolution rules; bundlers may support additional aliases. An import working in one environment does not guarantee it resolves in another.
// my-module.jsexport const myFunction = () => {/* ... */};export default myFunction;
// app.jsimport defaultFunction, { myFunction } from './my-module.js';myFunction();defaultFunction();
Exporting modules
Named exports
You can export multiple named items from a module. Use the export keyword before the function, variable, or class you want to export.
// myModule.jsexport const myFunction = () => {console.log('This is my function');};export const myVariable = 42;
Default exports
A module can have one default export. Use the export default keyword to export a single item.
// myModule.jsconst myFunction = () => {console.log('This is my function');};export default myFunction;
Importing modules
Importing named exports
To import named exports, use the import statement followed by curly braces containing the names of the exports.
// main.jsimport { myFunction, myVariable } from './myModule';myFunction(); // This is my functionconsole.log(myVariable); // 42
Importing default exports
To import a default export, use the import statement followed by a name of your choice.
// main.jsimport myFunction from './myModule';myFunction(); // This is my function
Importing all exports
You can import all named exports from a module using the * syntax and an alias.
// main.jsimport * as myModule from './myModule';myModule.myFunction(); // This is my functionconsole.log(myModule.myVariable); // 42
Dynamic imports
import() returns a Promise for the module namespace and can defer optional code:
async function openEditor() {const { createEditor } = await import('./editor.js');return createEditor(document.querySelector('#editor'));}
Handle loading errors and avoid splitting tiny dependencies without measuring the request and execution tradeoff.
Runtime behavior and common traps
- In a browser, load the entry with
<script type="module" src="./main.js"></script>. Relative imports normally include the file extension because they are URLs. - Imports are live, read-only views of exported bindings. The exporting module can update a
letbinding; importers see the new value but cannot assign to the import. - A module's top-level code normally executes once per resolved module instance. Avoid surprising side effects merely from importing a utility.
- Cyclic imports are allowed, but reading a binding before its initialization can fail. Refactor cycles that depend on execution order.
- A default export is not inherently better than named exports. Named exports can make auto-imports and refactors clearer when a module exposes several values.
- Do not place secrets in a browser module. Bundling or minification cannot make shipped credentials private.