cyberangles blog

How to Fix 'Class extends value #<Object> is not a constructor or null' in JavaScript: Resolving Circular Dependencies When Exporting/Importing Classes

If you’ve spent time working with JavaScript classes and modules, you’ve likely encountered cryptic errors that leave you scratching your head. One such error is:

Uncaught TypeError: Class extends value #<Object> is not a constructor or null

This error typically strikes when a class attempts to extend another class that isn’t available as a valid constructor at runtime. More often than not, the root cause is circular dependencies—a scenario where two or more modules import each other, creating a loop that confuses the JavaScript module loader.

In this blog, we’ll demystify this error, explore why circular dependencies trigger it, and provide actionable solutions to fix and prevent it. Whether you’re a beginner or an experienced developer, this guide will help you resolve this issue and write cleaner, more maintainable code.

2026-02

Table of Contents#

  1. Understanding the Error Message
  2. What Are Circular Dependencies?
  3. Why Circular Dependencies Trigger This Error
  4. Common Scenarios Where This Error Occurs
  5. How to Fix the Error: Practical Solutions
  6. Troubleshooting Tips
  7. Conclusion
  8. References

Understanding the Error Message#

Let’s start by breaking down the error:

Class extends value #<Object> is not a constructor or null

This error occurs when a class tries to extend a value that is either:

  • null or undefined (the parent class doesn’t exist), or
  • An object that isn’t a constructor (e.g., a plain object, array, or primitive).

In the context of module imports, this usually means the class you’re trying to extend hasn’t been fully initialized yet. Why? Because JavaScript modules load asynchronously, and circular dependencies can create a race condition where one module tries to use another before it’s ready.

What Are Circular Dependencies?#

A circular dependency happens when two or more modules import each other, directly or indirectly. For example:

  • Module A imports Module B.
  • Module B imports Module A.

Or indirectly:

  • Module A imports Module B.
  • Module B imports Module C.
  • Module C imports Module A.

JavaScript’s ES6 module system (ESM) supports circular dependencies, but it does so with "live bindings"—imported values update as the exporting module initializes. However, if a module tries to use an imported value before the exporting module has finished initializing, that value may not yet be a valid constructor (e.g., it could be undefined or an incomplete object).

Why Circular Dependencies Trigger This Error#

To understand why circular dependencies cause the "Class extends value..." error, let’s walk through a simplified example. Suppose we have two modules: User.js and Profile.js.

Example: Direct Circular Dependency#

User.js (exports User, imports Profile):

// User.js
import Profile from './Profile.js';
 
class User {
  constructor(name) {
    this.name = name;
    this.profile = new Profile(this); // Depends on Profile
  }
}
 
export default User;

Profile.js (exports Profile, imports User and extends it):

// Profile.js
import User from './User.js';
 
class Profile extends User { // Tries to extend User
  constructor(user) {
    super(user.name); // Calls User constructor
    this.bio = "No bio yet";
  }
}
 
export default Profile;

What Happens at Runtime#

When the application loads:

  1. The browser/Node.js starts evaluating User.js.
  2. User.js imports Profile.js, so evaluation pauses and switches to Profile.js.
  3. Profile.js imports User.js, which is already being evaluated. ESM allows this, but the User binding in Profile.js is initially a "live" reference that hasn’t been assigned yet.
  4. Profile.js tries to define Profile as a subclass of User. But User hasn’t finished initializing (since User.js is waiting for Profile.js), so User is either undefined or an empty object—not a constructor.

Result: TypeError: Class extends value #<Object> is not a constructor or null.

Common Scenarios Where This Error Occurs#

Circular dependencies often arise in codebases with tightly coupled classes. Here are two common scenarios:

Scenario 1: Mutually Dependent Classes#

As in the User and Profile example above: two classes where each depends on the other for initialization (e.g., User creates a Profile, and Profile extends User).

Scenario 2: Indirect Circular Dependencies#

A chain of imports that loops back:

  • A.js imports B.js.
  • B.js imports C.js.
  • C.js imports A.js.

In this case, A may not be ready when C tries to use it, triggering the error.

How to Fix the Error: Practical Solutions#

The best way to resolve this error is to eliminate circular dependencies. If that’s not feasible, use workarounds to defer or restructure dependencies. Below are actionable solutions.

Solution 1: Restructure Code to Remove Circular Dependencies#

The cleanest fix is to refactor your code to avoid circular imports entirely. This often involves:

  • Removing unnecessary dependencies.
  • Using composition instead of inheritance.
  • Combining tightly coupled modules.

Example: Replace Inheritance with Composition#

In the User/Profile example, the circular dependency exists because Profile extends User and User creates a Profile. If Profile doesn’t need to inherit from User, replace inheritance with composition:

Refactored User.js:

// User.js
import Profile from './Profile.js';
 
class User {
  constructor(name) {
    this.name = name;
    this.profile = new Profile(this); // Composition: Profile has a User, not extends it
  }
}
 
export default User;

Refactored Profile.js (no longer extends User):

// Profile.js
// No import of User (or import only for type checking)
class Profile {
  constructor(user) { // Accepts a User instance
    this.user = user; // Composition: Profile "has a" User
    this.bio = "No bio yet";
  }
}
 
export default Profile;

Now Profile no longer imports User to extend it, breaking the circular dependency.

Solution 2: Use Lazy Loading with Dynamic Imports#

If restructuring is difficult, use dynamic imports (import()) to load dependencies lazily (i.e., when needed, not at module startup). This defers the import until after the module has initialized.

Example: Lazy Import in a Method#

Modify Profile.js to import User dynamically inside a method instead of at the top level:

Profile.js (no top-level import of User):

// Profile.js
class Profile {
  constructor(userData) {
    this.userData = userData;
    this.bio = "No bio yet";
  }
 
  // Lazy-load User when needed
  async getParentUser() {
    const { default: User } = await import('./User.js');
    return new User(this.userData.name);
  }
}
 
export default Profile;

User.js (imports Profile normally):

// User.js
import Profile from './Profile.js';
 
class User {
  constructor(name) {
    this.name = name;
    this.profile = new Profile(this);
  }
}
 
export default User;

Now Profile no longer imports User at the top level, avoiding the circular dependency.

Solution 3: Extract Shared Logic to a Third Module#

If two modules depend on each other because they share logic, extract the shared code into a third module. This breaks the cycle by giving both modules a common, independent dependency.

Example: Extract Shared Logic#

Suppose User.js and Profile.js both use a validateName function. Extract this to UserUtils.js:

UserUtils.js (shared logic):

// UserUtils.js
export function validateName(name) {
  if (!name || name.length < 2) {
    throw new Error("Invalid name");
  }
}

User.js (imports UserUtils instead of Profile):

// User.js
import { validateName } from './UserUtils.js';
 
class User {
  constructor(name) {
    validateName(name);
    this.name = name;
  }
}
 
export default User;

Profile.js (imports UserUtils instead of User):

// Profile.js
import { validateName } from './UserUtils.js';
 
class Profile {
  constructor(user) {
    validateName(user.name); // Reuse shared logic
    this.bio = "No bio yet";
  }
}
 
export default Profile;

Now User and Profile depend on UserUtils, not each other.

Solution 4: Use Forward Declarations (TypeScript)#

If you’re using TypeScript, you can use type-only imports to reference a class without importing its implementation. This avoids circular runtime dependencies while preserving type safety.

Example: Type-Only Import in TypeScript#

Profile.ts:

// Import only the type, not the implementation
import type { User } from './User';
 
class Profile {
  user: User; // Type reference only
  bio: string;
 
  constructor(user: User) {
    this.user = user;
    this.bio = "No bio yet";
  }
}
 
export default Profile;

User.ts:

import Profile from './Profile';
 
class User {
  name: string;
  profile: Profile;
 
  constructor(name: string) {
    this.name = name;
    this.profile = new Profile(this);
  }
}
 
export default User;

TypeScript’s import type ensures the import is erased at runtime, breaking the circular dependency.

Troubleshooting Tips#

If you’re stuck, use these steps to diagnose the issue:

  1. Check for circular dependencies: Use tools like madge (a dependency graph generator) to visualize imports:

    npx madge --circular src/
  2. Log imported values: Temporarily add console.log statements to check if the imported class is undefined or an object:

    // In Profile.js
    import User from './User.js';
    console.log('User in Profile.js:', User); // Is this a constructor?
  3. Simplify the module: Temporarily remove code until the error disappears, then reintroduce it to isolate the dependency causing the loop.

Conclusion#

The "Class extends value # is not a constructor or null" error is a common symptom of circular dependencies in JavaScript modules. To fix it:

  1. Restructure code to remove circular imports (preferred).
  2. Use composition instead of inheritance.
  3. Extract shared logic into independent modules.
  4. Lazy-load dependencies with dynamic imports.

By following these steps, you’ll write more maintainable code and avoid the headaches of circular dependencies.

References#