Fix "X is not a constructor" from a plain require()
Why adding a property to exports and then requiring it without destructuring hands you an object instead of a constructor, reproduced on current Node.js.
TL;DR#
exports.User = mongoose.model(...) adds a User property to the module's exports object; it does not replace it. require('../Models/User') always returns that whole object, so const User = require(...) gives you { User: [Function] }, not the function itself — new User(...) then throws TypeError: User is not a constructor. const { User } = require(...) destructures the property out and works. The other fix is to stop adding a property and reassign module.exports directly.
TypeError: User is not a constructorWhy it happens#
This is the exact case from a Stack Overflow question with 15,211 views: a Mongoose model file exports like this.
// Models/User.js
const mongoose = require('mongoose');
exports.User = mongoose.model('User', {
email: { type: String, trim: true, minlength: 1, required: true },
});And the caller does a bare require:
// server.js
const User = require('../Models/User');
let user = new User({ email: 'test@example.com' }); // TypeError: User is not a constructorNode's module loader builds every CommonJS module around one real object: module.exports. Before your file's code runs, Node sets a local variable called exports to point at that same object, purely as a shorthand so you can write exports.foo = ... instead of module.exports.foo = .... require() never returns the local exports variable — it always returns module.exports, whatever that object looks like by the end of the file. The Node.js module documentation states this directly: "Assigning the desired object to exports will simply rebind the local exports variable, which is probably not what is desired", and gives the reference implementation of require() that makes it concrete — the module closure receives module and module.exports as its two parameters, and only writes to module.exports are visible to the caller.
So in Models/User.js, exports.User = mongoose.model(...) mutates module.exports by adding a User key to it — it does not replace module.exports with the model constructor. After the file runs, module.exports is { User: <Mongoose model constructor> }. require('../Models/User') in server.js returns exactly that object. const User = require(...) binds User (the local variable in server.js) to that whole object — an object, not a function — which is why typeof User is 'object' and new User(...) throws TypeError: User is not a constructor.
const { User } = require(...) is just an ordinary destructuring assignment on whatever require() returned; it is equivalent to const User = require('../Models/User').User. That pulls the .User property — the actual Mongoose model constructor — into the local User variable, which is why the identical new User(...) call then succeeds.
The negative control: reassigning the local exports variable does nothing#
A common but wrong guess is that writing exports = someFunction inside the module should also fix it, since it looks like it replaces "the export". It does not, and this is reproducible: exports is overwritten in place, but module.exports — the object require() actually returns — is untouched, so require() of that file still returns the original, unrelated object module.exports pointed to before the reassignment.
Fix#
There are two independent fixes; pick the one that matches how the caller imports the module.
1. Keep the property-based export, and destructure on import (no change to the model file):
// server.js
const { User } = require('../Models/User');
let user = new User({ email: 'test@example.com' }); // works2. Export the constructor directly, by reassigning module.exports (no change to the caller):
// Models/User.js
const mongoose = require('mongoose');
module.exports = mongoose.model('User', {
email: { type: String, trim: true, minlength: 1, required: true },
});// server.js
const User = require('../Models/User'); // now the model itself, not a wrapper object
let user = new User({ email: 'test@example.com' }); // worksDo not mix the two. The local exports variable is bound to the module's original module.exports object once, before the file's code runs, and that binding never updates itself afterwards. So once a line has reassigned module.exports to the model function, a later exports.User = ... in the same file does not attach to that function: module.exports.User = ... would, but exports.User = ... writes onto the abandoned original object instead, with no visible effect for the caller. Pick one shape for the module and keep every caller consistent with it.
If you cannot change the model file (a third-party package, for instance) but still want a single-name import, access the property explicitly instead of guessing at destructuring:
const UserModule = require('../Models/User');
let user = new UserModule.User({ email: 'test@example.com' });Order matters inside the model file too. The Node.js documentation is explicit that "assignment to module.exports must be done immediately" and "cannot be done in any callbacks" — if the reassignment happens inside a setTimeout, a promise callback, or any async handler that runs after require() has already returned the module's exports to the caller, the caller keeps the object it got at require time, not whatever module.exports points to later. This is unrelated to the destructuring question itself, but it is the same underlying contract: require() reads module.exports once, synchronously, as the module file finishes executing, and never looks at it again.
Verify the fix#
Reproduced on Node.js v26.7.0 (current, 2026-11-06), with one change at a time:
=== server-bare.js (const User = require('./model')) ===
typeof User (bare require): object
THROWS:
TypeError: User is not a constructor
=== server-destructured.js (const { User } = require('./model')) ===
typeof User (destructured require): function
OK, user = User { email: 'a@b.com' }
=== server-reassign.js (model-reassign.js: module.exports = User) ===
typeof User (module.exports reassigned): function
OK, user = User { email: 'a@b.com' }
=== server-reassign-exports-only.js (negative control: exports = User, local var only) ===
typeof User (local exports var reassigned only): object
value: {}Four files were held fixed except for the one line under test. model.js keeps the SO question's shape, exports.User = User; model-reassign.js reassigns module.exports = User directly; model-reassign-exports.js only reassigns the local exports variable (exports = User), never touching module.exports. Running the same new User({ email: ... }) call against each shows the full matrix: bare require against exports.User = User throws, destructured require against the same file works, bare require against module.exports = User works, and bare require against exports = User returns the original empty object, not the reassigned function — because that write never reached module.exports.
The last run is the negative control for the "reassign exports instead" guess: the module body executed exports = User (rebinding the local variable only), and require() still returned the original empty object module.exports pointed to, with typeof User staying 'object'. That confirms the mechanism holds on a current Node.js release, not only on the Node version the original 2018 question ran on — the module loader's contract between module.exports and the exports shorthand has not changed since it was documented at v0.1.16.
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.
A related gotcha: exports works until it doesn't#
The same confusion shows up with pure ES module interop. If a package ships both a CommonJS build with exports.default = ... and claims ESM named exports, require() on the CommonJS side returns the object with .default on it, not the thing you expect at the top level — the fix follows the same rule: either destructure the named property you need, or import the whole module object and read the property explicitly. See Fix: exports is not defined in ES module scope for the companion case where Node's syntax detection decides a file is an ES module and the bare exports shorthand is not available at all, and drizzle-kit/api: Dynamic require of "fs" is not supported for what happens when a package's own build calls require() from inside a file Node already treats as an ES module. If switching a project's module files between .js and .mjs to work around this kind of mismatch trips an ESM-only plugin elsewhere, Fix ERR_REQUIRE_ESM in gulp: ESM-only plugins, old gulp-cli covers the next error in that chain.
This mechanism is older than any specific framework: it is how require() has worked since CommonJS modules were introduced in Node.js, and it applies identically whether the exported value is a Mongoose model, an Express router, or a plain class — the only thing that matters is whether the file replaced module.exports or merely added a property to it. The second-highest-voted answer on the original question makes the same point from the other direction: keeping const User = require('../Models/User') as a bare import is fine as long as every call site then writes User.User, because that is honestly what the file exports — an object with one property on it, not a constructor. The error only appears when the caller's code assumes the shape the destructured import would have given it while actually holding the wrapper object underneath.
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: 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.
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.
Fix: Directory import is not supported resolving ES modules
Why Node throws ERR_UNSUPPORTED_DIR_IMPORT under "type": "module", three fixes reproduced on Node 24, and why the old specifier flag no longer helps.


