TL;DR
Two files are importing each other. When Node.js loads them, one module tries to extend a class that hasn't been exported yet โ so it gets undefined instead. The fix: pull the shared base class into its own file and import from there.
What the error looks like
TypeError: Class extends value undefined is not a constructor or null
at Object.<anonymous> (/app/models/Dog.js:3:19)
at Module._compile (node:internal/modules/cjs/loader:1356:14)
The stack trace always points to a class Foo extends Bar line. Bar is undefined when that line runs โ not because you forgot to export it, but because the file exporting it hasn't finished loading yet.
Root cause: the circular load order problem
CommonJS modules are cached the moment they start loading. When two files import each other, one gets a half-baked, partially-evaluated version of the other. Here's the exact pattern that triggers it:
// Animal.js
const { Dog } = require('./Dog'); // โ pulls in Dog first
class Animal {}
module.exports = { Animal };
// Dog.js
const { Animal } = require('./Animal'); // โ Animal is {} here โ not yet exported
class Dog extends Animal {} // TypeError: Class extends value undefined
module.exports = { Dog };
Here's the sequence: Node starts loading Dog.js, which triggers Animal.js. Immediately, Animal.js requires Dog.js โ but Dog.js is already mid-load in the cache, so Node hands back whatever has been exported so far: an empty object {}. Destructuring { Animal } from that empty object yields undefined. The error fires the instant extends executes.
Fix 1: Extract the base class into its own file (recommended)
Move the parent class to a file that no child class depends on. Clean, obvious, works every time.
// base/Animal.js โ new file, zero circular imports
class Animal {
speak() {
return 'Some sound';
}
}
module.exports = { Animal };
// Dog.js
const { Animal } = require('./base/Animal'); // โ straightforward import
class Dog extends Animal {
speak() {
return 'Woof';
}
}
module.exports = { Dog };
// Animal.js (kept for re-exporting or other logic)
const { Animal } = require('./base/Animal');
const { Dog } = require('./Dog');
module.exports = { Animal, Dog };
A useful rule to pin up: base classes should never import from their subclasses. If Animal.js needs to reference Dog, that's a design smell. Dependencies should flow downward, not back up.
Fix 2: Remove the unnecessary import
Sometimes the parent file only imports a subclass to re-export it โ nothing more. Ask whether that import actually needs to happen at load time.
// Before: Animal.js drags in Dog for no functional reason
const { Dog } = require('./Dog');
class Animal {}
module.exports = { Animal, Dog };
// After: consumers import Dog directly
class Animal {}
module.exports = { Animal };
Fix 3: Defer the require() call (CommonJS only)
Sometimes you genuinely need both classes to know about each other โ just not at module evaluation time. In that case, move require() inside a method body:
// Animal.js
class Animal {
createDog() {
const { Dog } = require('./Dog'); // โ runs only when called, not on load
return new Dog();
}
}
module.exports = { Animal };
By the time createDog() gets called, both modules have fully loaded and the cache holds complete exports. That said, this is a workaround. If you can restructure the code instead, do it โ deferred requires scatter dependencies across the codebase and make refactoring harder.
Finding circular dependencies automatically
Don't hunt for cycles by reading files. madge does it in seconds:
npx madge --circular --extensions js src/
TypeScript projects:
npx madge --circular --extensions ts src/
Output looks like this:
Circular dependency found!
models/Dog.js โ models/Animal.js โ models/Dog.js
Wire it into CI so cycles never sneak in again:
npx madge --circular src/ && echo "No circular deps" || exit 1
ESM (import/export) note
ESM uses live bindings rather than cached snapshots, so circular imports behave differently. You can still hit this exact error, though โ if a class is referenced before its declaration is evaluated, you're back to undefined.
// ESM version of the same problem
// animal.mjs
import { Dog } from './dog.mjs'; // dog.mjs imports animal.mjs โ cycle
export class Animal {}
// Fix: base.mjs imports nothing from subclasses
export class Animal {}
Same root cause, same solution.
Verify the fix
- Run
npx madge --circular src/โ confirm zero cycles reported. - Start the app:
node index.jsornpm startโ theTypeErrorshould be gone. - Run the test suite:
npm testโ class instantiation tests should all pass. - Jest users: if the error appeared in tests, clear the module cache first with
jest --clearCache, then re-run.
Quick checklist
- Does the parent class file import from a child class? Remove it.
- Do two files share a type or interface? Pull it out to
types.jsor abase/directory. - Using a barrel file (
index.js) that re-exports everything? Barrel files hide circular deps easily โ import directly from the source file instead. - Did
madgefind the cycle? Fix the shortest link in the chain first, then recheck.

