Engineering

What Does `require` Do in JavaScript?

What does require do in JavaScript? See how it resolves, loads, and caches a module, and when to use import instead in 2026.

SA
Samer Alsayegh
Founder
Published
5 min read

require() loads a module and returns its exports, synchronously, right where you call it. It's CommonJS's core function, Node's original module system from 2009, and it still runs by default in any Node file without a "type": "module" in package.json.

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

That's the whole surface most developers use daily. The interesting parts are what happens underneath, and where require and ESM's import genuinely diverge.

What Does require() Actually Do?

Four things happen on every require() call. Node resolves the module path, using its resolution algorithm (relative paths, node_modules lookup, package.json main or exports fields). It reads and wraps the file in a function. It executes that function, capturing whatever gets assigned to module.exports. It caches the result, so a second require() of the same path returns the cached exports instead of re-running the file.

// Node wraps every CommonJS file in something like this
(function(exports, require, module, __filename, __dirname) {
  // your file's code runs here
});

That wrapper is why module, exports, require, __filename, and __dirname are available in every CommonJS file without an import. They're function parameters, not globals.

require vs import: The Real Differences

require() is synchronous and runs anywhere in a file, including inside a function or an if block. import is static. It must sit at the top level, and its declarations get hoisted before any code runs. That's what lets bundlers analyze a dependency graph without executing code first.

// require: works anywhere, including conditionally
if (isDev) {
  const debug = require('debug');
}

// import: must be top-level, hoisted before this runs
import debug from 'debug'; // can't be inside an if block

import also gives you live bindings. If a module updates an exported value, every importer sees the new value automatically. require() copies the exports object at call time, so a later mutation on the source module doesn't reach code that already destructured the export.

Why import needs file extensions and require doesn't

CommonJS resolution guesses: require('./utils') tries ./utils.js, ./utils.json, and a few other extensions automatically. ESM resolution is strict. import './utils' fails unless you write import './utils.js', the full extension. That's because ESM's resolver is spec'd to behave identically in a browser, where there's no file system to probe.

Can CommonJS and ESM Modules Use Each Other?

Partially, and the direction matters. ESM can import from a CommonJS package. Node handles that interop automatically, wrapping the CommonJS module.exports as a default export.

The reverse used to be impossible. Node 22 added require(esm), letting a CommonJS file synchronously load an ES module for the first time. That closed a real gap. Before it, a CommonJS codebase that needed one ESM-only dependency had no clean way to consume it, short of an async import() call breaking the synchronous flow.

// Node 22+: CommonJS can now require an ES module
const esmThing = require('esm-only-package');

Should New Projects Use require or import?

Use import, with "type": "module" in package.json, for anything new. Most of the npm ecosystem is shipping ESM-only releases, and starting a project in CommonJS means fighting that trend from day one instead of with it.

require() still matters for two real cases. Maintaining an existing CommonJS codebase not worth a full migration. Writing quick Node scripts where synchronous, anywhere-in-the-file loading beats top-level-only import.

What Errors Does require Throw?

Two errors show up constantly, and they mean different things. MODULE_NOT_FOUND means Node's resolution algorithm walked every candidate path and found nothing: a typo'd package name, a missing npm install, or a relative path that's wrong from the file's actual location, not the one you're picturing.

require('./utils'); // MODULE_NOT_FOUND if ./utils.js doesn't exist

ERR_REQUIRE_ESM is the more confusing one. It fires when a CommonJS file tries to require() a package that only ships ESM, before Node 22's require(esm) support existed for that case. The fix, before that support landed, was either an async import() or waiting for the package to ship a CommonJS build. On modern Node, the same call may now just work.

Reading the error name literally saves debugging time here. MODULE_NOT_FOUND is a path problem. ERR_REQUIRE_ESM is a module-format mismatch, not a missing file, and no amount of checking your spelling fixes it.

The Underused Angle

Most require explainers stop at syntax and skip the caching behavior that causes real bugs. require()'s module cache is keyed by resolved file path. require('./config') from two different directories can return two different cached instances of what looks like the same module, if Node resolves them to different physical files. A singleton pattern built on require()'s caching silently breaks the moment a project's folder structure changes underneath it.

That's not a reason to avoid require. It's a reason to know the cache is path-based, not name-based. Know that before relying on it for a singleton, a shared config, or a database connection meant to exist exactly once.

Conclusion

require() synchronously resolves, loads, executes, and caches a CommonJS module, wrapped in a function that's why module and __dirname feel like magic globals. import trades that flexibility for static analysis, live bindings, and strict resolution. New projects should default to import; existing CommonJS code doesn't need an urgent rewrite just to keep up. If you're building a Node backend that serves content to a frontend either way, Draftbase's typed SDK works the same whether your project runs CommonJS or ESM.

Ship content that's built to be found

Draftbase generates schema, structured data, and a fast MDX editor for every post.

Frequently asked questions

What does require do in JavaScript?

It resolves a module's file path, loads and runs the file, and returns whatever got assigned to module.exports. The result gets cached for later calls.

Is require synchronous or asynchronous?

Synchronous. It blocks and returns the module's exports immediately, which is why it can run inside a function or an if block, unlike import.

Can require load an ES module?

Yes, on Node 22 and later, using require(esm). Before that, a CommonJS file had no clean synchronous way to load an ESM-only package.

Why does my code throw ERR_REQUIRE_ESM?

It means a CommonJS file tried to require a package that only ships ES modules. That's a format mismatch, not a missing file or a typo.

Should I still use require in new projects?

No. Use import with type module in package.json instead. Most of npm is shipping ESM-only now, so new projects should default to it.

SA
Samer Alsayegh
Founder at Draftbase

Samer is a software engineer and entrepreneur, founder of Draftbase and Ezi Home Services, building technology that simplifies home services. Passionate about software, APIs, automation, and creating products that solve real-world problems.

javascriptnodejs

Related posts

Draftbase is a headless CMS built for React devs.