Menu
Try in Playground

CommonJS vs ES Modules: require vs import in JavaScript

The two module systems JavaScript ships with, why both exist, and how to choose between require and import in Node projects.

Two Module Systems, One Language

JavaScript originally had no module system at all. Node filled the gap in 2009 with CommonJS (require, module.exports), and for years that's what Node code looked like. Then in 2015, the language itself grew a standard module system — ES Modules (import, export) — which browsers and Node now both support.

So you'll see both in the wild. Here's the same tiny module written each way:

index.js
Output
Click Run to see the output here.
index.js
Output
Click Run to see the output here.

Same function, two different envelopes. The rest of this page is about when each envelope matters and which one to reach for.

The Syntax Differences

The day-to-day differences fit on a postcard:

// CommonJS
const fs = require("fs");
const { readFile } = require("fs/promises");

module.exports = something;
module.exports.name = value;
exports.name = value;
// ES Modules
import fs from "fs";
import { readFile } from "fs/promises";

export default something;
export const name = value;
export { name };

require is a regular function call. import is a statement — it can only appear at the top level of a module, and the path has to be a string literal. That restriction isn't arbitrary; it's what lets ESM do things CommonJS can't.

The Real Difference: Static vs Dynamic

CommonJS evaluates require() when the line runs. You can put it inside an if, compute the path at runtime, load a module conditionally:

index.js
Output
Click Run to see the output here.

ES Modules are static. The engine parses all the import statements before running any code, builds a dependency graph, and resolves everything upfront. That's why the path must be a literal string and why import only appears at the top level.

The payoff: tools can see the whole module graph without executing anything. That's how bundlers do tree-shaking (dropping unused exports), how editors give you accurate autocomplete, and how the browser can fetch modules in parallel.

When you really do need dynamic loading in ESM, use import() — a function-like expression that returns a Promise:

index.js
Output
Click Run to see the output here.

How Node Decides Which System a File Uses

A single Node project can contain both kinds of files. Node figures out which system each file uses by looking at two things:

  • The file extension: .mjs is always ESM, .cjs is always CommonJS.
  • The nearest package.json's "type" field: "module" means .js files are ESM, "commonjs" (the default if missing) means they're CJS.
// package.json
{
    "name": "my-app",
    "type": "module"
}

With "type": "module", a plain hello.js in the same package uses import/export. Drop a hello.cjs alongside it and that one file uses require. This is how a project can gradually migrate, or how a library can ship both flavors side by side.

Beginner gotcha: in an ESM file, require and module.exports simply don't exist. If you reach for them out of muscle memory you'll get a ReferenceError.

Interop: Mixing the Two

You'll often need an ESM file to use a CommonJS package, or vice versa. The rules are asymmetric.

ESM importing CommonJS works directly. The CJS module.exports object becomes the default export:

index.js
Output
Click Run to see the output here.
// app.mjs
import greet from "./greet.cjs";
console.log(greet("Rosa"));

Named imports from CommonJS work sometimes — Node tries to detect named exports statically — but for reliability, grab the default and destructure:

import pkg from "./utils.cjs";
const { parse, stringify } = pkg;

CommonJS importing ESM is the painful direction. You can't require() an ES module — it throws ERR_REQUIRE_ESM. The escape hatch is dynamic import(), which works in CJS too and returns a Promise:

index.js
Output
Click Run to see the output here.

Modern Node (22+) added a synchronous require() for ESM under certain conditions, but dynamic import() is the portable answer.

Other Behavioral Differences Worth Knowing

Beyond syntax, the two systems disagree on a few details that occasionally bite:

index.js
Output
Click Run to see the output here.

A few more:

  • this at the top level. In CJS, this is module.exports. In ESM, it's undefined. ESM is always in strict mode.
  • __dirname and __filename. CJS gives you these for free. In ESM you derive them from import.meta.url:
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
  • File extensions in imports. ESM requires the extension ("./utils.js", not "./utils") for relative paths. CJS is forgiving.
  • Live bindings vs snapshots. ESM imports are live references to the exporting module's variables. CJS gives you a copy of whatever was assigned to module.exports at load time. Most code never notices, but it matters for circular dependencies.

Which One Should You Use?

For a new project: ES Modules. Set "type": "module" in package.json and don't look back. ESM is the language standard, works the same way in browsers and Node, supports top-level await, and tools are built with it in mind.

Stick with CommonJS when:

  • You're maintaining an existing CJS codebase and the migration isn't worth it yet.
  • You're publishing a library that needs to support very old Node versions or consumers that can't use ESM.
  • A critical dependency only ships CJS and its interop story is messy. (Rare now, but still happens.)

Even then, you'll be reading ESM code constantly — everything published on npm over the last few years trends that way. Being fluent in both isn't optional; being fluent in the idioms of the one you're actually writing is.

A Quick Mental Checklist

When you open a new file, ask:

  • Does this file use import/export or require/module.exports? Don't mix them.
  • What does the nearest package.json say about "type"?
  • If you're importing a package, check its package.json — does it ship ESM, CJS, or both?
  • If you hit ERR_REQUIRE_ESM, you're in CJS trying to load ESM. Switch to dynamic import() or move the caller to ESM.

Ninety percent of module confusion in Node is one of those four.

Next: npm Basics

Modules are how you split your code across files. The next step is pulling in code other people wrote — that's what npm is for. We'll cover installing packages, semver ranges, and the parts of the npm workflow you actually use day-to-day.

Frequently Asked Questions

What's the difference between require and import in JavaScript?

require is the CommonJS way of loading modules — synchronous, runs at the point it's called, and returns whatever the module assigned to module.exports. import is the ES Modules syntax — static, hoisted to the top of the file, and analyzed before the code runs. They also disagree on what this is, how circular dependencies resolve, and whether top-level await is allowed.

Should I use CommonJS or ES Modules in a new Node project?

Use ES Modules. Set "type": "module" in package.json and write import/export. ESM is the official standard, works in browsers and Node, and supports top-level await. CommonJS still shows up in older packages and tooling, so you'll read it even if you don't write it.

Can I mix require and import in the same project?

Yes, with rules. A .mjs file or a package with "type": "module" uses ESM; .cjs or "type": "commonjs" uses CJS. ESM can import a CommonJS module (the module.exports shows up as the default export). CommonJS can't require() an ESM module directly — you have to use dynamic import(), which returns a Promise.

Learn to code with Coddy

GET STARTED