Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

CommonJS and ES Modules Compared: Syntax, Semantics, UMD, and Edge Cases

Tech 1

CommonJS and ES Modules are the dominant JavaScript module systems in use today, with UMD frequently employed as a compatibility layer for libraries that target both Node.js and browsers.

Module formats in practice

  • UMD (Universal Module Definition)
  • CommonJS (primarily Node.js)
  • ES Modules (standardized in ECMAScript)

UMD wrapper (browser + Node)

(function (globalScope, factory) {
  if (typeof module === 'object' && module.exports) {
    // Node/CommonJS
    module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
    // AMD
    define([], factory);
  } else {
    // Browser globals
    globalScope.Library = factory();
  }
}((typeof self !== 'undefined' ? self : this), function () {
  'use strict';
  const api = { version: '1.0.0' };
  return api;
}));

CommonJS (Node.js)

  1. Export patterns
  • module.exports is the actual exported value
  • exports is a convenience alias that initially references module.exports
// file: util.js

// Property-style exports
module.exports.count = 1;
module.exports.bump = function () { module.exports.count += 1; };
exports.msg = 'hello';

// Replacing the entire export object
module.exports = {
  count: 1,
  msg: 'hello',
  bump() { this.count += 1; }
};

// Pitfall: reassigning `exports` breaks the link with `module.exports`
exports = {  // ❌ does not change what the module actually exports
  count: 1,
  msg: 'hello'
};
  1. Import via require
// file: main.js
const util = require('./util');
console.log(util.count); // 1
util.bump();
console.log(util.count); // 2
  1. Cyclic dependencies in CommonJS CommonJS executes modules immediately on first require, and exports are ordinary values at the moment of assignment. During a cycle, a module may observe a partially initialized export from its dependency.
// file: alpha.js
exports.value = 'alpha:v1';
const beta = require('./beta');
console.log('alpha sees beta.flag =', beta.flag);
exports.value = 'alpha:v2';

// file: beta.js
exports.flag = 'beta:v1';
const alpha = require('./alpha');
console.log('beta sees alpha.value =', alpha.value);
exports.flag = 'beta:v2';

// file: main.js
const alpha = require('./alpha');
console.log('main sees alpha.value =', alpha.value);

ES Modules (ESM)

Core export forms

// Re-export everything
export * from './other.js';

// Re-export specific bindings (renaming allowed)
export { a, b as bAlias } from './other.js';

// Named exports from local declarations
export const x = 1, y = 2;
export function add(n, m) { return n + m; }
export class Box {}

// Default export (expression)
export default function greet(name) { return `hi ${name}`; }

// Default export as a live binding via rename
let counter = 0;
function inc() { counter += 1; }
export { counter as default, inc };

Core import forms

// Named imports
import { x, y as why } from './math.js';

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

// Namespace import
import * as pkg from './pkg.js';

// Mixed
import def, { x as x1, add } from './mod.js';

// Side-effect-only import (no bindings pulled in)
import './polyfill.js';

// Dynamic import (asynchronous)
const modPromise = import('./feature.js');
modPromise.then(mod => mod.run());

Static structure constraints

  • import and export are only valid at the module’s top level
  • Bindings are resolved statically at parse/compile time
  • Names in import/export cannot be computed dynamically

Invalid examples

// ❌ Not allowed inside blocks or control flow
if (true) {
  import { x } from './m.js';
  export const y = 1;
}

// ❌ Not allowed: computed or string-literal as the imported name
const name = 'x';
import { [name] } from './m.js';
import 'defaultName' from './m.js';

Live bindings and read-only imports

  • All ESM modules run in strict mode
  • Imported bindings are read-only views into the exporter’s variables
  • Updates in the exporting module are observable by importers
// file: counter.mjs
export let ticks = 0;
export function tick() { ticks += 1; }

// file: main.mjs
import { ticks, tick } from './counter.mjs';
console.log(ticks); // 0
tick();
console.log(ticks); // 1

// file: snap.mjs
let score = 0;
export default score;      // default export of the current value (snapshot)
export function inc() { score += 1; }

// file: consume.mjs
import value, { inc } from './snap.mjs';
console.log(value); // 0
inc();
console.log(value); // 0 (default export of expression is not a live binding)

// file: live-default.mjs
let level = 0;
export { level as default }; // live binding for default via rename
export function bump() { level += 1; }

// file: use-live.mjs
import lvl, { bump } from './live-default.mjs';
console.log(lvl); // 0
bump();
console.log(lvl); // 1

Cyclic dependencies in ES Modules ESM initializes the module graph in dependency order and wires live bindings before evaluation. Accessing an imported binding before the exporter initializes it triggers a temporal dead zone error or yields uninitialized state. Avoid reading imports at top level during a cycle; delay use to a function.

Problematic pattern

// file: a.mjs
import { bVal } from './b.mjs';
console.log('a sees bVal =', bVal); // ⚠️ may throw if bVal not initialized
export const aVal = 'A';

// file: b.mjs
import { aVal } from './a.mjs';
console.log('b sees aVal =', aVal); // ⚠️ may throw if aVal not initialized
export const bVal = 'B';

Safe pattern (defer usage)

// file: a.mjs
import { ping } from './b.mjs';
export function pong() { return 'pong'; }
export function runA() { return `A got: ${ping()}`; }

// file: b.mjs
import { pong } from './a.mjs';
export function ping() { return `ping + ${pong()}`; }

// file: main.mjs
import { runA } from './a.mjs';
console.log(runA());

Key differences: CommonJS vs ES Modules

  • Binding semantics
    • CommonJS exports are snapshots of values at the time of assignment; reassignment requires writing back to module.exports
    • ES Modules export live bindings (except export default expression, which is a value). export { name as default } creates a live default binding
  • Syntax and timing
    • CommonJS is dynamic; require can be used anywhere and is synchronous
    • ES Modules are static at parse time; import/export must be top-level, with module loading and evaluation order managed by the engine. Dynamic import() is asynchronous
  • Multiplicity of exports
    • CommonJS has a single exported value (module.exports)
    • ES Modules support multiple named exports plus a optional default
  • this at top level
    • In CommonJS modules, top-level this is module.exports (in Node’s wrapper)
    • In ES Modules, top-level this is undefined

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.