Skip to content
beginner

Use JavaScript ES Modules in the Browser with import and export

The moment your script file gets long enough that you scroll to find a function, you have outgrown the single-file habit.

Published 2026-10-03Updated 2026-10-0412 min read
Detailed close-up image of a lizard resting on a rock, showcasing its textured skin in a natural setting.
Detailed close-up image of a lizard resting on a rock, showcasing its textured skin in a natural setting. Photo by Marc Onana on Pexels.

The moment your script file gets long enough that you scroll to find a function, you have outgrown the single-file habit.

You already know how to attach a JavaScript file to an HTML page with a <script> tag. That works fine for a while. Then you reuse a function on a second page, and now you have two copies. Fix a bug in one, forget the other, and the second page keeps the old behavior. Silent, annoying, and completely avoidable.

The fix is a module: a file that keeps its own private scope and shares only what it chooses to share. You declare what leaves the file with export, and you pull it into another file with import. That is the whole idea.

There is one condition that trips up nearly everyone the first time: modules need to be served over a real address, not opened by double-clicking the HTML file. We will get to exactly why, but keep it in the back of your mind.

Why One Big Script File Stops Working

A single <script> tag is a great starting point. It is also a trap that closes slowly.

The first symptom is length. You scroll to find things. The second symptom is duplication. You need formatPrice() on a second page, so you copy it. Now there are two versions of the same logic, and they will drift apart the first time you fix one and forget the other.

A module solves both problems with one rule: a file only exposes what it exports. Everything else stays private to that file. No accidental globals, no name collisions, no "where did this variable come from?"

If you have not yet set up an editor and browser for running JavaScript, do that first. This article assumes you can already create an HTML file, a .js file, and open the browser console.

Your First Module: One Export, One Import

An HTML page loads main.js as a module; main.js imports greet.js, which exports the greet function.
The browser starts at the HTML module script, then follows imports to load the files they depend on.

Let's build the smallest possible working example. Two files, one function, one import.

Create a file called greet.js:

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

The export keyword in front of function is what makes greet available to other files. Without it, greet would exist only inside greet.js.

Now create main.js in the same folder:

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

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

Two things to notice. First, the curly braces around greet — that is the named import syntax, and the name must match the export exactly. Second, the path includes the file extension: './greet.js', not './greet'. The browser does not guess extensions for you.

Finally, create index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Modules Demo</title>
  </head>
  <body>
    <script type="module" src="./main.js"></script>
  </body>
</html>

The type="module" attribute is the switch. It tells the browser to treat main.js as a module rather than a regular script. You do not add a second <script> tag for greet.js — the browser fetches it automatically because main.js imports it.

Before you can see the output, you need to serve these files over http://. The quickest way is a local server. If you have Node.js installed, open a terminal in your project folder and run:

npx serve

Then open the URL it prints, usually something like http://localhost:3000. Open the browser console, and you should see:

Hello, Ada!

If you see that line, the wiring works. Everything else in this article is variation on this pattern.

Knowledge check

Check your understanding

Answer this question before you continue.

With the article's `greet.js` and `main.js` example loaded as a module, what does the console show?
Output Prediction

Focus: Predict the console output when a browser module imports and calls an exported function.

Named Exports vs. Default Exports

There are two ways to export from a file, and beginners often pick the wrong one by accident. Here is the practical difference.

Named exports use curly braces on both sides. You can have as many as you want in one file, and the names must match between the export and the import.

Default export is one per file. It is imported without braces, and you can name it whatever you like at the import site.

// math.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export default function multiply(a, b) { return a * b; }
// main.js
import multiply, { add, subtract } from './math.js';

console.log(add(2, 3));
console.log(subtract(5, 2));
console.log(multiply(4, 4));
5
3
16

Notice that multiply comes before the braces. That is the default export. The named exports sit inside the braces.

You can also rename a named import with as when the original name is awkward or collides with something in your file:

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

And when a file exports several related things and you want them grouped, use the namespace form:

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

Here is the comparison in one table:

StyleSyntaxHow many per fileImport shapeRename
Namedexport function foo() {}Manyimport { foo } from './x.js'import { foo as bar }
Defaultexport default function foo() {}Oneimport foo from './x.js'Name it anything at import

My rule: use a default export for the one obvious thing a file provides, and named exports for everything else. If a file is called formatDate.js and exports one function, default is fine. If a file is called utils.js and exports five helpers, use named exports so the reader knows exactly what is coming in. Whatever you pick, stay consistent within a project — mixed conventions are how teams end up arguing in code review.

Knowledge check

Check your understanding

Answer this question before you continue.

Given the article's `math.js`, which import brings in the default `multiply` export and the named `add` and `subtract` exports?
Single Choice

Focus: Distinguish the import syntax for a default export from the syntax for named exports.

The Error You Will Hit: Opening the HTML File Directly

Here is the part that catches almost every beginner. You double-click index.html, the page loads, and the console shows something like:

Access to script at 'file:///.../greet.js' from origin 'null' has been blocked by CORS policy

Your code is not wrong. The protocol is wrong.

When you open a file by double-clicking, the browser loads it over file://. Modules are blocked there for security reasons — a module can fetch other files, and the browser refuses to let a local file pull in arbitrary resources from your disk. The fix is not to change your code. The fix is to serve the files over http://.

But file:// is not the only cause of module-loading trouble. Different error messages point to different problems, and reading the right one saves you from guessing. Here is how to tell them apart:

Error messageWhat it meansWhere to look
blocked by CORS policy with a file:// URLThe page was opened directly from diskServe the folder over http://
Expected a JavaScript module script but the server responded with a MIME type of "text/html"The server sent the wrong content type, often because the path resolved to an HTML error pageCheck the Network tab for the actual response
Cannot use import statement outside a moduleThe <script> tag is missing type="module"Add the attribute to the script tag
Failed to resolve module specifier or a 404The import path is wrongCheck the extension and the relative path

The MIME error is worth a second look because it is easy to misread. If the server responds with text/html for a .js request, the browser is probably receiving a 404 page instead of your file. The path is wrong, not the server configuration. Check the Network tab and look at what actually came back.

Rule of thumb: if the URL bar starts with file://, that is the bug. If it starts with http:// and you still see an error, read the message — it is telling you which of the other three problems you have.

Knowledge check

Check your understanding

Answer this question before you continue.

A page opened by double-clicking `index.html` shows a `blocked by CORS policy` error for a `file://` URL. What should you do first?
Debugging

Focus: Identify direct file opening as the cause of a module CORS error and select the taught remedy.

Run It Through a Local Server

Any static server works. The lowest-friction option for a beginner is the live server feature built into most modern editors — in VS Code it is the Live Server extension, and in many editors it is built in.

If you have Node.js installed, you can also run one from the terminal in your project folder:

npx serve

Then open the URL it prints, usually something like http://localhost:3000. You should see the same Hello, Ada! in the console that failed a moment ago.

Two things to verify once it works:

  1. Open the Network tab in your browser's developer tools and reload. You should see main.js and greet.js as two separate requests. That is visible proof the import resolved.
  2. Check that the server sends text/javascript as the content type for .js files. Most static servers do this by default, so you usually do not need to think about it — but if you see a MIME type error, this is the cause.

Keep this loop short. The goal is a working page, not a server administration project.

Knowledge check

Check your understanding

Answer this question before you continue.

After starting a local server and reloading the example, which Network-tab result is the article's expected proof that the import resolved?
Single Choice

Focus: Use the browser Network tab to confirm that an imported module dependency was fetched.

How Modules Change the Rules Inside Your File

Once the example runs, a few behavioral differences show up that are worth understanding before you write more code.

Each module has its own top-level scope. A variable declared at the top of greet.js is not visible in main.js unless you export it. This is the whole point — no more accidental globals leaking onto window.

Modules run in strict mode automatically. Strict mode turns some silent mistakes into loud errors. Assigning to an undeclared variable, for example, throws instead of quietly creating a global. That is a good thing, but it can surprise you if you are used to sloppy-mode behavior.

A module is evaluated once. If two files import the same module, its top-level code runs only the first time. The second import gets the already-initialized result. This is why you should keep top-level code for setup — creating a config object, registering something — and export functions for anything you need to call repeatedly.

That last point is the payoff. You are trading a shared global namespace for isolated files that declare their own boundaries. The extra setup step buys you a codebase that does not collapse into a pile of name collisions.

When Modules Are the Right Tool — and When They Are Not

Modules are not always the answer. Here is how I decide.

Use modules when:

  • You are reusing a function across more than one page.
  • A single file has more than one clear responsibility and you want to separate them.
  • You are working with another person and need clear boundaries between your code and theirs.

Skip modules when:

  • You have one small script on one page. The extra file and the server step buy you nothing.
  • You are prototyping something you will throw away in an hour.

One clarification that saves confusion later: modules are not a build tool. They are the browser's native mechanism for splitting code. Bundlers like Vite or webpack exist for other reasons — combining many files into one request, transforming syntax, optimizing for production. You do not need any of that to start using modules.

When you are ready to go further, two features are worth knowing by name: import maps, which let you import by a short name instead of a relative path, and dynamic imports, which load a module on demand with import() inside a function. You do not need either today. Just know they exist so the path does not feel like a dead end.

Practice: Split a Working Script Into Modules

Here is a task that forces you to do the split yourself.

Start with a single script that defines two functions and calls them on a button click:

// app.js
function showGreeting() {
  console.log('Hello from the greeting button');
}

function showFarewell() {
  console.log('Goodbye from the farewell button');
}

document.getElementById('greet').addEventListener('click', showGreeting);
document.getElementById('farewell').addEventListener('click', showFarewell);

Your task:

  1. Move showGreeting into greeting.js and export it.
  2. Move showFarewell into farewell.js and export it.
  3. Create a new main.js that imports both and wires up the buttons.
  4. Load main.js with <script type="module">.
  5. Serve the folder and confirm both buttons still log the same messages.

If nothing happens when you click, check the URL bar first — is it file://? Then check your import paths and extensions. The console will tell you which one is wrong.

Extension: add a third module that imports from greeting.js and calls it. You should see the dependency chain resolve in the Network tab as three separate requests.

What You Just Learned

You started with a script file that was getting too long to navigate. You ended with code that lives in files declaring their own boundaries, wired together with import and export, running through a local server.

The decision rule to carry forward: if you are copying a function into a second page, it belongs in a module. That single instinct will keep your codebase from quietly doubling itself every time you add a page.

Your next step is to take a script you have already written — anything with two or more functions — and split it into modules. Do it once by hand, watch the Network tab, and the pattern will stick. From there, the natural continuation is learning how to fetch data from an API and display it, which is where modules start paying off in real projects.

Knowledge check

Final check

Finish the article by checking the ideas you just learned.

Two files import the same module. Which statement matches the article's description of module evaluation?
Question 1 of 2Misconception Check

Focus: Explain what happens when multiple files import the same module.

You need the same function on a second page. According to the article's decision rule, what is the appropriate next step?
Question 2 of 2Misconception Check

Focus: Decide when the article recommends using a module to avoid duplicating shared code.

References

  1. JavaScript-Module - JavaScript | MDNdeveloper.mozilla.org
  2. Modules, introductionjavascript.info
  3. JavaScript import maps are now supported cross-browser  |  Blog  |  web.devweb.dev
Practical resource

Want a more structured JavaScript path?

Use the JavaScript for AI Applications Starter Pack to turn individual tutorials into a focused path from language fundamentals to interactive AI applications.

View the bundle
Coming soon

JavaScript for AI Applications Starter Pack

Build the JavaScript foundation behind modern interactive applications. The JavaScript for AI Applications Starter Pack takes you from core language concepts through functions, browser events, forms, DOM updates, application state, debugging, and practical projects—so you can learn how interfaces take input, work with structured data, respond to users, and turn results into experiences people can actually use.

$9
PDF BundleJavaScriptWeb DevelopmentAI Applications
  • 253-page illustrated PDF
  • 12 guided JavaScript chapters
  • Visual concept diagrams
  • Self-assessment quizzes
  • Bonus deep-dive sections
  • DOM, events, forms & application state
  • Browser projects, debugging & practical workflows

Coming soon

Keep learning

Related tutorials

Continue with nearby JavaScript topics and beginner-friendly explanations.