Skip to content

Node.js: require is not defined in ES Module Scope

Fix Node's 'ReferenceError: require is not defined in ES module scope' and its __dirname sibling using createRequire and import.meta.

nodejs nodejs-errors commonjs es-modules import-meta nodejs-26
Bharath G
Reading Progress

On This Page

The Error

file:///app/index.js:1
const fs = require('node:fs');
           ^

ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/app/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
    at file:///app/index.js:1:12
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5)

Node.js v22.22.2

Its sibling throws the moment code reaches for the other CommonJS-only globals:

file:///app/index.js:1
console.log(__dirname);
            ^

ReferenceError: __dirname is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/app/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
    at file:///app/index.js:1:13
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5)

Node.js v22.22.2

__filename throws the identical shape of error, just naming __filename instead of __dirname.

This is the classic "yargs on Node 26" crash pattern currently showing up on GitHub (see GoogleCloudPlatform/artifact-registry-npm-tools#88): a package ships plain .js files that call require(), gets loaded as ESM because the nearest package.json says "type": "module", and the CommonJS globals aren't there. It also shows up constantly under webpack#18580 and in the long tail of "fix __dirname is not defined" write-ups from bobbyhadz, GeeksforGeeks, and builtin.com — this is one of the highest-volume CJS→ESM migration errors in search traffic, second only to the module-resolution errors already covered in this series.

I verified the exact stack trace above by running the repro on Node.js 20.20.2, 21.7.3, and 22.22.2 in this session — the message text and the "This file is being treated as an ES module because..." hint are byte-identical across all three; only the internal frame names for the ESM loader shift (node:internal/modules/esm/module_job line numbers move, and the entry-point frame name changes between versions). The hint line itself was added specifically to stop people from filing "TypeError, require is broken" bugs, and it correctly diagnoses the two most common causes: a "type": "module" field, or a .mjs extension.

How to Reproduce It

Directory layout:

app/
├── package.json
└── index.js

package.json — the "type": "module" field is what turns every .js file in this tree into an ES module:

json

{
  "name": "repro-require-esm",
  "version": "1.0.0",
  "type": "module"
}

index.js (ESM, because of the "type" field above):

javascript

const fs = require('node:fs');
console.log(fs.readFileSync('./package.json', 'utf8'));

Run it:

bash

node index.js

Output is the first stack trace shown in Section 1. The __dirname variant reproduces the same way — swap the body of index.js for console.log(__dirname); and rerun.

You get the identical error three other ways, all equally common in the wild:

  • The file's own extension is .mjs, regardless of any "type" field — .mjs always forces ESM parsing.
  • No "type" field exists at all, but Node's automatic module-type detection (see Section 3) sniffs ESM syntax in a sibling file or the entry file itself and treats the whole run as a module.
  • A dependency ships CommonJS-style .js files but the consuming app's package.json sets "type": "module", which is exactly the yargs-on-Node-26 case in the GitHub issue above: yargs's CJS entry point gets loaded through the ESM loader because of the importer's package configuration, and require isn't in scope inside it.

Useful flags while debugging this class of error: NODE_DEBUG=esm node index.js prints every step the ESM loader takes (module translation, dependency graph construction) so you can see exactly which file got classified as ESM and why; node --trace-warnings surfaces related deprecation warnings if the fix you reach for touches a deprecated API.

Version Behaviour Matrix

This error's message text and trigger conditions are stable across every currently supported line — Node has not changed the wording since .mjs/"type": "module" parsing was introduced. What changes release to release is which workarounds exist and whether you hit the error at all:

Node.js lineStatus (as of Sept 2026)Behaviour
20 (Iron)EOL 2026-04-30Error unchanged. import.meta.dirname/import.meta.filename available but experimental until 20.11.0 raised it, still flagged experimental in this line.
22 (Jod)Maintenance LTS, EOL 2027-04-30Error unchanged. import.meta.dirname/import.meta.filename no longer experimental as of 22.16.0 — safe to depend on.
24 (Krypton)Active LTS, EOL 2028-04-30Error unchanged. import.meta.dirname/import.meta.filename stable (24.0.0+). require(esm) — the opposite direction, CommonJS calling require() on an ES module — is unflagged by default here too, which resolves a large class of related but distinct interop failures (not this one).
26Current, released 2026-05-05Same error, same fix. require(esm) is fully non-experimental as of the 25.4.0/26.x line. This is the version in the live GitHub issue cited above — the crash is not a Node 26 regression, it's a package (yargs) that never accounted for being loaded as ESM.
27First release under the new one-major-per-year schedule; 27.0.0-alpha.x builds available from October 2026No planned change to this error. Every line becomes LTS-track under the new schedule, so once 27 ships there is no more odd/even split to reason about.

Confirmed live: import.meta.dirname and import.meta.filename were added behind no flag at all in v21.2.0/v20.11.0, then had their "experimental" label formally lifted in v24.0.0 and v22.16.0 — I ran the repro on 20.20.2 and it printed a correct, non-experimental-warning value, matching the docs. __dirname/__filename/require/module/exports being absent from ES modules is not a bug or a version-specific gap to track — it's the ECMAScript module specification itself; Node never injects CommonJS-only bindings into a module the spec says is a "module record," at any version, past or future.

Why It Happens — Surface Level

require, module, exports, __filename, and __dirname were never JavaScript language features. They're CommonJS-specific locals that Node's CJS loader injects into every .js file it treats as CommonJS. The moment a file is classified as an ES module instead — via "type": "module" in the nearest package.json, a .mjs extension, or Node's own auto-detection sniffing import/export syntax — that injection never happens, because ES modules are parsed and linked as standard ECMAScript module records with no such locals. Code that assumes it's running as CommonJS and reaches for any of these five names gets a ReferenceError, not a TypeError: from the parser's point of view, require and __dirname are simply undeclared identifiers.

Why It Happens — Under the Hood

Look at how Node actually builds a CommonJS module before execution. node:module's Module.wrap() does this, verified live in this session:

javascript

> require('node:module').wrap('//code')
'(function (exports, require, module, __filename, __dirname) { //code\n});'

Every CommonJS file Node loads gets textually wrapped in that function and invoked with those five values as real function parameters — require is a closure bound to that specific module's resolution context (which is why require.cache, relative resolution, and require.resolve all work per-file), and __dirname/__filename are just strings computed from the file's path at wrap time. None of this is language-level scope; it's a function call.

ES modules skip this machinery entirely. The V8-level ModuleWrap that backs an ES module is a Source Text Module Record per the ECMAScript spec — it has its own lexical environment populated by the module's own import/export bindings and nothing else. There is no wrapper function, so there are no extra parameters to receive require or __dirname into. Running NODE_DEBUG=esm node index.js against the repro shows the loader building this record explicitly:

ESM 2130: Translating StandardModule file:///app/index.js
ESM 2130: Storing file:///app/index.js (implicit type) in ModuleLoadMap
ESM 2130: ModuleJob.run() ModuleWrap { sourceURL: undefined, sourceMapURL: undefined, url: 'file:///app/index.js', isMain: true }

Notice it's a ModuleWrap, not the CJS Module object with its wrapper-injected locals — a structurally different object, not the same one missing a property.

This is also why the fix isn't "polyfill the missing globals" — you can't reassign lexical bindings that were never declared, and even if you could, require()'s behavior depends on a per-file resolution scope that only exists inside the CJS loader's closure. createRequire() (below) works around this by manufacturing an equivalent closure yourself, scoped to whatever URL you hand it, rather than resurrecting the implicit one.

One clarification worth making explicit because it trips people up: require(esm) — Node's feature (stable by default since the 22.12/23.1 line, formally non-experimental as of 25.4.0) that lets CommonJS code call require() on an ES module — solves the opposite direction of interop. It does nothing for this error, because this error happens inside a file that is already an ES module trying to use a CommonJS-only name. That's a separate, larger topic on its own (ERR_REQUIRE_ESM and require(esm) interop), left for its own article.

To Fix

For require(): manufacture a scoped require with node:module's createRequire(), passing in the current module's URL:

-const fs = require('node:fs');
+import { createRequire } from 'node:module';
+
+const require = createRequire(import.meta.url);
+const fs = require('node:fs');

Verified working on both Node 20.20.2 and 22.22.2 in this session — createRequire(import.meta.url) returns a fully functional require, including require.resolve and its own require.cache, scoped exactly as if the file were CommonJS.

If the module you're loading is itself pure ESM, don't reach for createRequire at all — use a real import (top-level, or dynamic await import() if you need it conditionally):

-const fs = require('node:fs');
-fs.readFileSync('./package.json', 'utf8');
+import fs from 'node:fs';
+fs.readFileSync('./package.json', 'utf8');

For __dirname / __filename: on Node 20.11.0/21.2.0 and later, use import.meta.dirname and import.meta.filename directly — no imports needed:

-console.log(__dirname);
-console.log(__filename);
+console.log(import.meta.dirname);
+console.log(import.meta.filename);

Confirmed live: on Node 20.20.2, 21.7.3, and 22.22.2, import.meta.dirname printed the correct absolute directory and import.meta.filename the correct absolute path with no warning.

If you need to support Node versions older than 20.11/21.2, or want the pre-import.meta.dirname idiom that still appears in a lot of published packages, derive them from import.meta.url instead:

javascript

import { fileURLToPath } from 'node:url';
import path from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

Also verified live — identical output to import.meta.dirname/import.meta.filename on all three tested versions.

If the file was never meant to be ESM at all (a config script, a build tool plugin, a file some other tool generated as CommonJS), the fastest fix is to stop it from being parsed as a module: rename it from .js to .cjs, which forces CommonJS regardless of the "type" field, exactly as the error's own hint text suggests.

Which fix to use: reach for createRequire/import.meta.dirname when the file is genuinely meant to be ESM and just needs one CommonJS-only capability; rename to .cjs when the whole file is CommonJS in spirit and got swept into "type": "module" by an ancestor package.json; migrate the whole file to import/export when you're touching it anyway and want to stop straddling both module systems.

Practices & Better Design

Pick one module system per package and say so explicitly — set "type": "module" or "type": "commonjs" in package.json rather than leaving Node to guess, and keep any files that must stay CommonJS on the .cjs extension so their type never depends on where they sit in the tree. Within ESM files, standardize on import.meta.dirname/import.meta.filename (Node 20.11+/21.2+) instead of re-deriving them from import.meta.url in every file — it's shorter, and it removes an import you'd otherwise need in files that don't use node:url for anything else.

javascript

// config.js — package.json has "type": "module"
import { readFile } from 'node:fs/promises';

const configPath = new URL('./config.json', import.meta.url);
const config = JSON.parse(await readFile(configPath, 'utf8'));

export default config;

This runs as written on Node 20.20.2 through 26.x: it never touches require, __dirname, or __filename, and it resolves the sibling file relative to the module's own URL rather than process.cwd() — which also sidesteps the entire class of "works when I run it from the project root, breaks from anywhere else" bugs that cwd-relative paths cause.

For a package you publish to npm and expect others to require() and import, declare both entry points explicitly in exports rather than relying on extension or "type" guessing to sort it out for consumers — that keeps this exact error from becoming your users' problem instead of yours.

Prevent It in the Long Term

Lint for it before it ships: n/no-unsupported-features/node-builtins and n/no-missing-import (both from eslint-plugin-n) catch a require call sitting in a file ESLint's sourceType: 'module' parser already knows is a module, and a plain no-undef rule with the parser's sourceType set correctly will flag bare __dirname/__filename/require/module/exports references in ESM files as undefined identifiers — the same way it would flag any other typo. Run node --check (or just node itself, since this throws immediately at load time) against every entry point in CI, not just the ones you remember to test, since this error only surfaces the moment the file is actually loaded.

At the packaging level, publint and arethetypeswrong catch the case where your own package's "type", "exports", and file extensions disagree with each other before you publish something that will throw this exact error for every consumer who imports it the "wrong" way. If you maintain a monorepo with mixed CJS and ESM packages, keep an explicit table of which package is which and enforce it with a CI check that greps for "type" in each package.json, rather than trusting that nobody will ever add a .js file assuming the wrong default.

Important

  • require, module, exports, __filename, and __dirname are not language globals — they're function parameters Node's CommonJS loader injects via Module.wrap(), and ES modules never go through that wrapper.
  • The trigger is always one of three things: a .mjs extension, an ancestor package.json with "type": "module", or Node's own ESM-syntax auto-detection — the error message names which one applied.
  • Fix require with createRequire(import.meta.url) from node:module; fix __dirname/__filename with import.meta.dirname/import.meta.filename (stable since Node 22.16.0/24.0.0, available unflagged since 20.11.0/21.2.0).
  • This is unrelated to require(esm) (CommonJS loading an ES module) — that solves the opposite direction and doesn't touch this error at all.
  • When the file is fundamentally CommonJS and got swept into ESM by an ancestor's"type": "module", renaming it to .cjs is faster and clearer than adding interop shims.
nodejsnodejs-errorscommonjses-modulesimport-metanodejs-26

From aspiring developer to blogger, I test learning platforms, simplify programming syntax, and share resources that work. Helping you code smarter as I grow myself. New or experienced, you're welcome here.

Comments