Fix 'TypeError: Class extends value undefined is not a constructor or null' from Circular Dependency in Node.js

intermediate๐Ÿ’š Node.js2026-04-13| Node.js 12+, any OS (Linux, macOS, Windows), ES6 classes with CommonJS or ESM modules

Error Message

TypeError: Class extends value undefined is not a constructor or null
#Node.js#JavaScript#Circular Dependency#ES6 Classes

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.js or npm start โ€” the TypeError should 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.js or a base/ 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 madge find the cycle? Fix the shortest link in the chain first, then recheck.

Related Error Notes