Fix: exports is not defined in ES module scope
How Node decides CommonJS vs ES module, and three remedies for aligning source syntax, compiler output and the runtime.
TL;DR#
Node decides CommonJS vs ES module per file, using the nearest
package.json, the extension and, for ambiguous files, the syntax. Once a
package sets "type": "module", its plain .js files are parsed as ES
modules. A .js file without an explicit type can also become ESM when
Node detects module syntax. ES modules have no exports,
module, or require in scope. A file that still writes
exports.foo = … throws
ReferenceError: exports is not defined in ES module scope the instant it
runs; module.exports = … similarly fails because module is not defined.
If the file contains only CommonJS syntax, rename it to .cjs (keeps the CommonJS
syntax, Node always loads .cjs as CommonJS regardless of "type"), or by
rewriting it with export/import syntax so it matches what "type": "module" expects. If it already mixes ESM statements and CommonJS
assignments, convert the assignments to ESM; renaming such a mixed file
to .cjs leaves invalid ESM statements in a CommonJS file. Pick the change that matches how the
file is actually written.
The error#
ReferenceError: exports is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '\\?\C:\aotd-repro-exports-undefined\package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
at file:///C:/aotd-repro-exports-undefined/main.js:1:1
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)This is the exact message from a minimal reproduction on Node.js 26.7.0: a
package.json with "type": "module" next to a main.js that still opens
with exports.hello = function hello() { … }. Node names the file and the exact
package.json it blamed, which is the fastest way to confirm this diagnosis
on your own project — the path in the second line of the error is the file
Node actually read "type" from, not necessarily the one at your project
root (a nested package.json wins over the one above it).
It is usually the second error in a pair. The original Stack Overflow
question
started from a NestJS project that imported a package shipped only as .mjs
(@nuxt/vite-builder-edge). require()-ing that .mjs file first threw
Error [ERR_REQUIRE_ESM]: require() of ES Module … not supported. Adding
"type": "module" to package.json "fixed" that one — and immediately broke
every file in the project that still used exports/module.exports
(including TypeScript's own compiled output), which is where exports is not defined in ES module scope comes from.
Why it happens#
Node decides this per file, documented on the package.json and file
extensions page:
the nearest package.json's explicit "type" field controls how plain .js
files in that package are parsed — "commonjs" means CommonJS;
"module" means ES modules. A missing type is ambiguous: modern Node can
detect module-only syntax, such as an export statement or top-level await,
and load that file as ESM. The syntax detection documentation
explains this case; omitting the field does not force CommonJS. Two extensions override the field
outright, on the same page: a .mjs file is always an ES module and a
.cjs file is always CommonJS, no matter what "type" says.
So once a project sets "type": "module", three things are simultaneously
true, and the error comes from mixing them:
.jsfiles are now parsed as ES modules — norequire,exports,module,__dirnameor__filenamein scope..cjsfiles are still CommonJS, unconditionally.- A TypeScript project compiled with
"module": "commonjs"intsconfig.jsonstill emitsexports.foo = …andrequire(...)into its output.jsfiles —tscdoes not look at the runtime"type"field when choosing what to emit. The compiled file and the runtime disagree about what kind of module it is, and the runtime wins: Node parses the file as ESM because"type": "module"says so, hits the firstexports.statement, and throws.
That third point is the one the NestJS case hit: the compiler kept emitting
CommonJS syntax while the runtime had been told everything is now an ES
module. Under "module": "commonjs", nothing about adding "type": "module"
to package.json changes what the compiler emits — that is a second,
independent setting, which is exactly the mismatch that throws. (Under
"module": "node16"/"nodenext", tsc does look at the nearest "type"
field per file and switches its emitted syntax to match — that newer setting
is the fix in point 3 below, not the default most existing projects start
from.)
Fix#
Pick the remedy that matches what actually produced the broken file.
1. The file is meant to stay CommonJS — rename it to .cjs#
If only one or two files in an otherwise-ESM project still use
require/exports (a build script, a config file), renaming is the
smallest change:
mv main.js main.cjsReproduced: a file containing exports.hello = function hello() { return "hi"; }; under "type": "module" throws the exact error above when run as
main.js, and prints hi with no error once renamed to main.cjs — nothing
else in the file changed.
If the file is required elsewhere by a bare specifier, update that one
import line too (require('./main.cjs') or import './main.cjs'); Node
does not resolve ./main to ./main.cjs automatically.
2. The whole package should be CommonJS — set its type explicitly#
If "type": "module" was added only to silence ERR_REQUIRE_ESM on one
dependency, and the rest of the project is CommonJS (common in an existing
NestJS/Express codebase), set "type": "commonjs" explicitly and
instead load that one ESM-only dependency with a dynamic import() — which
the ERR_REQUIRE_ESM message itself already names as the supported path for
CommonJS code reaching into an ES module. Dynamic import expressions
are valid in CommonJS, including at the top level when the returned promise
is handled with .then(). Awaiting one belongs inside an async function:
CommonJS does not support top-level await. An explicit CommonJS package
type prevents syntax detection from reclassifying the file. For an awaited
result, call it from an async function:
// instead of: const { bundle } = require('@nuxt/vite-builder-edge');
async function loadBundler() {
const { bundle } = await import('@nuxt/vite-builder-edge');
return bundle;
}For TypeScript, inspect the emitted JavaScript as well: compiling this
expression with the CommonJS module target can turn it back into
require(). Use an appropriate Node-aware module mode
that preserves dynamic import in CommonJS output, such as NodeNext, and
check the generated file before treating this as a fix for an ESM-only
dependency. Changing the runtime package type alone does not change what
the compiler emits.
This remedy was not run in the reproduction below — it is the documented
behaviour of import() from CommonJS, not a tested fix for this article.
Reproduced instead, as the negative control: the same CommonJS style of
code — module.exports.hello = …, not the exports.hello = … shorthand
that fails under "type": "module" — run under a sibling package.json
with explicit "type": "commonjs" runs
unchanged, printing hi2 with no error.
3. The compiled output should be ESM — change what tsc emits, not the file#
If the project is TypeScript and "type": "module" is staying, the compiler
needs to stop emitting exports.foo = …. In tsconfig.json, change
"module" from "commonjs" to an ES module target ("ES2020", "ES6", or
the newer "NodeNext", which also matches Node's own resolution rules) so
tsc emits export/import statements instead of exports/require calls.
This is exactly what the accepted answer on the original question did.
Reproduced: a file using export function hello() { … } (the shape tsc
emits under an ES module target) runs with no error under "type": "module"
— same runtime, same file extension, only the statement shape changed.
One side effect worth knowing before you flip that switch: Node's own ESM
resolver, unlike tsc's classic CommonJS resolution, does not add missing
file extensions. A relative import that worked as import './app.module'
under "module": "commonjs" needs its extension spelled out —
import './app.module.js' — once the output is ESM, even though the source
file is app.module.ts. This is the next thing the Stack Overflow asker hit
immediately after switching, and it is a separate, expected change in
resolution behaviour, not a new bug.
Verify the fix#
Run the entry point directly and confirm there is no ReferenceError, then
check the specific statement shape in the file that used to fail:
node path/to/the/file/that/threw.js
grep -n "^exports\.\|^module.exports" path/to/the/file/that/threw.jsIf the chosen remedy was to emit ESM but that grep still finds CommonJS
assignments, inspect whether stale output remains. CommonJS assignments
are expected when the chosen remedy was .cjs or an explicit CommonJS
package type. On a TypeScript project, re-run tsc --build --force after editing tsconfig.json: a stale compiled .js file in dist/
can still carry the old exports. lines even after the source .ts and the
config have been fixed.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Check your own module error#
exports is not defined in ES module scope is one specific symptom of a
broader family of ESM/CommonJS mismatches — ERR_REQUIRE_ESM, Cannot use import statement outside a module, ERR_UNKNOWN_FILE_EXTENSION ".ts", and
a handful of others all trace back to the same root cause: some part of the
toolchain expected CommonJS and got an ES module, or vice versa. Paste the
exact error below to see which side of the mismatch you are on:
Runs entirely in your browser — nothing you paste is sent anywhere. Tokens and passwords that appear in paths or URLs are masked in the output.
Optional: package.json, file extension, Node.js version (sharper advice)
The same family shows up with a different symptom in native ESM tests:
Jest's jest object needs explicit access, and using jest.fn()
without that import throws ReferenceError: jest is not defined — see
the fix for that one
if switching a project to ESM breaks its test suite next.
For a plain JavaScript file with one module format, test the relevant file
extension or package-type change before making unrelated edits. A TypeScript
build may need its compiler output and runtime settings aligned together;
check the emitted file rather than assuming a package-type edit handles
both. If the compiler's output
shape is the actual cause (point 3 above), TS2305: Module Has No Exported
Member covers a separate
compile-time import/export mismatch, before the file runs. A separate subpath-resolution
error in the same family is Fix
ERR_PACKAGE_PATH_NOT_EXPORTED —
governed by the package's "exports" map and whether the caller used
require() or import, not by the same "type" mechanism as this error.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix ERR_MODULE_NOT_FOUND: relative import missing .js
Why an Express app with "type": "module" cannot find ./config/app, why the flags from older answers now do nothing, and how TypeScript catches it at compile time.
Fix Vite: local module 'does not provide an export named'
Reproduced with Node's own ES module loader: why the browser's loader finds no export in a tsc CommonJS build, and why two of the three moduleResolution values the top answer lists don't compile.
Fix Day.js: dayjs_1.default is not a function
Separate a successful type check from a callable runtime import, and avoid the namespace workaround that fails when interop is enabled.


