EcmaScript modules
Introduction
No no no. Not the layout modules. The JavaScript modules! They are still modular.
But – we do seem to run out of names to call things pretttty quickly.
Your little JavaScript app’s JS file got really long didn’t it? Luckily, we have a new way to break up JS files.
ES?
That’s Ecma Script.
This new “modules” ability was added to the spec in 2015, but it wasn’t until 2020 that the browsers had enough support for this style of modularizing code.
Using ES modules in the browser
<!doctype html>
<html>
<head>
<title>JS module exploration</title>
</head>
<body>
<h1>Hello.</h1>
<output></output>
<script src='app.js' type='module'></script>
</body>
</html>
For the JavaScript file to use these new tricks, you have to tell it it’s special. Nothing crazy. Just a new attribute.
// extremely awesome helper library
export default function double(number) {
return number * 2;
}
To prepare the code to be imported in another file, you have to export it. There are many ways to do this. Here’s one. You’re exporting it and specifically saying that when you import it – this function will be the default thing it imports. Nothing crazy. Just a couple of keywords.
import myLibrary from './library.js';
const outlet = document.querySelector('output');
outlet.innerHTML = myLibrary(5); / should show 10 on the screen
You import that file and give what you are importing a name (like a variable – because if it didn’t have a name you would have no way to refer to it). Just like a reference/variable or function, you get to decide what it is.
But this is a little confusing. How would you know what the library was going to do exactly from this view point? Anyway – go type it all out and try it before you move on.
To use modules in the browser, you’ll need to tell the script to behave as a module and you’ll need to have a server running. You can use MAMP for this, even though you probably think about it as a PHP thing.
Why do we need a server? Good question!
Making things a bit easier to read
// extremely awesome helper library
export default function double(number) {
return number * 2;
}
Instead of exporting this one function as the default, you can use a code block to export it down below.
// extremely awesome helper library
function double(number) {
return number * 2;
}
export default {
double
}
This is a nice way to organize things so you don’t muck up your functions. And this way, you can export many things.
import library from './library.js';
const outlet = document.querySelector('output');
outlet.innerHTML = library.double(5);
So, when you import the library, you’re making all the exports available as an object. This is what we’re used to with array methods and console.logs etc.
Finding a happy place
// extremely awesome helper library
function double(number) {
return number * 2;
}
function triple(number) {
return number * 3;
}
export {
double,
triple,
}
You can skip the whole default assignment here and just clearly decide what functions you want to export.
Explore how the export works in a sandbox.
import { double, triple } from './library.js';
const outlet = document.querySelector('output');
outlet.innerHTML = double(2) + triple(3); // 4 + 9 = 13
This special maneuver is called “destructuring.” Here, it’s being used to unpack the object. It takes those exports right out of their parent and assigns them to variables all in one go. Slick!
Explore object destructuring in a sandbox.
In the world of JavaScript modules, libraries, and frameworks – you’re going to see A LOT of this stuff. So, spend the time today to get really comfortable with how this works.
There are a lot of interesting ways you can export code from one module and use it in another. But until you have reason to get all fancy, just stick with this style.
Choosing what to expose
// extremely awesome helper library
const SOME_NUMBER = 879486;
function _double(number) {
return number * 2;
}
function _triple(number) {
return number * 3;
}
function doUltimateMath(number) {
return _double(number) + _triple(number) * SOME_NUMBER;
}
export {
doUltimateMath,
}
JavaScript files will have all sorts of values and functions that you won’t want to expose. You can keep some private in a sense – and only export the things you expect to make available in other files.
One convention you’ll see is an underscore before a function that’s really only meant to be used within other functions. This way, your doUltimateMath function can use all the other little functions – and you can keep everything tidy and only export the things you want.
Another convention shown here is all caps for a variable that will not change. These are “constants” and are dually noted by the use of the const keyword instead of the var keyword and the ALL_CAPS_SNAKE_CASE. But these are just some conventions. There are no rules saying you have to do it like this.
import { doUltimateMath } from './library.js';
const outlet = document.querySelector('output');
outlet.innerHTML = doUltimateMath(3);
Is this feeling clear?
Many many imports
// render mania! makes rendering HTML easy!
function render(where, what) {
where.innerHTML = what;
}
export {
render,
}
Have you heard of RenderMania? It’s a hot new JavaScript package that allows you to render content on an HTML page! We have got to download it – and include it in our project. It’s pretty complicated – but you might be able to understand what it does.
import { doUltimateMath } from './library.js';
import { render } from '/render-mania.js';
render( document.querySelector('output'), doUltimateMath(5) );
It’s not uncommon to have 3-10 imports at the top of a file.
And really, each library might import a bunch of other libraries.
OK! So, this is fairly new to the browser. A lot of history had to go down over the past 10 years to get here. This is a pretty fabulous syntax. Get used to it!
Go take your JS project (that’s probably a really really long page) and try and break it up into smaller pieces. Get lots of practice. See how many imports and exports you can use – and have everything work properly.
Side-Effect-Only Import in ESM
import './tests/cart.test.js';
This pattern executes the imported module immediately without assigning it to a variable. Instead of importing specific exports, it runs top-level code as soon as the module loads.
It’s ideal for test suites, polyfills, or setup scripts—a simple, one-off import where you just need the code to run.
Common pitfalls
These common issues are part of the learning curve with ES modules. Don’t worry if they come up—understanding and troubleshooting them is just part of the process. With each one, you’ll improve at spotting patterns and developing solutions that make your projects run more smoothly.
-
Forgetting to start a server
If you open index.html directly from your file system, modules won’t load properly. Make sure you’re running your project on a simple server. You can use MAMP or a browser extension like Live Server to serve files over HTTP. If you’re comfortable using the terminal, tools like http-server or Python’s SimpleHTTPServer are also quick options.
-
Using incorrect paths
Skipping ./ or using incorrect paths causes import errors. Always use relative paths like ./utils.js to avoid issues and ensure files load correctly.
-
Mixing CommonJS and ES modules
You might come across documentation that mixes require() (CommonJS) and import (ESM). This can cause conflicts since the two systems aren’t fully compatible. To avoid issues, stick with one module system throughout your project. ES modules are the modern standard, so it’s a good idea to use them for consistency and future-proofing.
-
CORS issues with external modules
When importing external resources, you might encounter CORS restrictions. Use trusted CDNs or configure your server to allow cross-origin requests for seamless imports.
-
Overusing default exports
Default exports can lead to confusion if you rename them during imports. Stick to named exports unless a file only contains one primary export, keeping your imports predictable.
-
Circular dependencies
If two modules import each other, you’ll encounter undefined values. Break the loop by moving shared logic into a separate helper module to maintain clear dependencies.