Skip to content

Fix: NestJS Circular Dependency — forwardRef and Module Design

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix NestJS circular dependency errors — using forwardRef, restructuring module dependencies, extracting shared services, and understanding the NestJS module system.

The Error

NestJS throws a circular dependency error on startup:

[Nest] ERROR [ExceptionHandler] Nest cannot create the module instance.
Caught a "Circular dependency" error while processing provider "UserService".
Please, make sure that each side of a circular dependency has been decorated with an "forwardRef()" decorator.

Or a less obvious dependency loop:

ERROR [ExceptionHandler] A circular dependency between modules has been detected.
Please, make sure that each side of a "forwardRef()" decorator is used.

The modules that form this cycle:
UsersModule -> PostsModule -> UsersModule

Or NestJS silently creates providers as undefined:

TypeError: Cannot read properties of undefined (reading 'findUser')
// userService is undefined because of unresolved circular dep

Why This Happens

Circular dependencies occur when two or more providers or modules depend on each other, forming a cycle. The fundamental issue is that NestJS’s dependency injection container must instantiate providers in a specific order. If UserService requires PostService to be constructed, and PostService requires UserService to be constructed, neither can go first. The container reaches a deadlock.

This is not unique to NestJS. Every dependency injection framework faces this problem. The difference is in how they handle it. NestJS fails loudly at startup, which is actually preferable to frameworks that fail silently at runtime. The error message tells you exactly which provider or module cycle was detected, which makes the fix straightforward once you understand the pattern.

The most insidious variant is the indirect cycle: A depends on B, B depends on C, and C depends on A. Three-link cycles are harder to spot because no single file shows the full loop. The cycle only becomes visible when you trace the import chain across files.

Specific triggers:

  • Provider circular dependencyUserService injects PostService, and PostService also injects UserService. NestJS can’t instantiate either because each requires the other to exist first.
  • Module circular dependencyUsersModule imports PostsModule, and PostsModule imports UsersModule. Same deadlock at the module level.
  • Indirect circular dependencyA to B to C to A. The cycle may span three or more classes, making it harder to spot.
  • Missing forwardRef() — even when the circular dep is intentional and unavoidable, both sides need forwardRef() so NestJS can resolve them lazily.

Fix 1: Use forwardRef for Provider Circular Dependencies

forwardRef() tells NestJS to use a lazy reference — the actual class is resolved after all providers are registered:

// WRONG — direct circular injection, NestJS can't resolve
// users.service.ts
@Injectable()
export class UserService {
  constructor(private postService: PostService) {}  // Circular
}

// posts.service.ts
@Injectable()
export class PostService {
  constructor(private userService: UserService) {}  // Circular
}
// CORRECT — use forwardRef() on BOTH sides
// users.service.ts
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { PostService } from '../posts/post.service';

@Injectable()
export class UserService {
  constructor(
    @Inject(forwardRef(() => PostService))
    private postService: PostService,
  ) {}

  async getUserWithPosts(userId: number) {
    const posts = await this.postService.findByAuthor(userId);
    return { userId, posts };
  }
}
// posts.service.ts
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { UserService } from '../users/user.service';

@Injectable()
export class PostService {
  constructor(
    @Inject(forwardRef(() => UserService))
    private userService: UserService,
  ) {}

  async findByAuthor(userId: number) {
    const user = await this.userService.findOne(userId);
    return this.postRepository.find({ where: { authorId: user.id } });
  }
}

Common Mistake: Adding forwardRef() only on one side. Both sides of the circular dependency must use forwardRef(). If only one side uses it, NestJS still can’t resolve the cycle.

Fix 2: Use forwardRef for Module Circular Dependencies

When two modules import each other, use forwardRef() on both module imports:

// WRONG — direct circular module import
// users.module.ts
@Module({
  imports: [PostsModule],   // Circular
  providers: [UserService],
  exports: [UserService],
})
export class UsersModule {}

// posts.module.ts
@Module({
  imports: [UsersModule],   // Circular
  providers: [PostService],
  exports: [PostService],
})
export class PostsModule {}
// CORRECT — forwardRef() on both module imports
// users.module.ts
import { forwardRef, Module } from '@nestjs/common';
import { PostsModule } from '../posts/posts.module';

@Module({
  imports: [forwardRef(() => PostsModule)],   // Lazy module reference
  providers: [UserService],
  exports: [UserService],
})
export class UsersModule {}
// posts.module.ts
import { forwardRef, Module } from '@nestjs/common';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [forwardRef(() => UsersModule)],   // Lazy module reference
  providers: [PostService],
  exports: [PostService],
})
export class PostsModule {}

Fix 3: Refactor to Break the Cycle (Preferred)

forwardRef() is a workaround. The better solution is redesigning the dependency structure to eliminate the cycle. Circular dependencies often indicate a violation of the Single Responsibility Principle — two modules are too tightly coupled.

Extract a shared service into a separate module:

Before (circular):
UsersModule <-> PostsModule

After (no cycle):
UsersModule -> SharedModule
PostsModule -> SharedModule
// shared/shared.module.ts — contains logic needed by both modules
@Module({
  providers: [NotificationService, AuditService],
  exports: [NotificationService, AuditService],
})
export class SharedModule {}

// users.module.ts — imports SharedModule, not PostsModule
@Module({
  imports: [SharedModule],
  providers: [UserService],
  exports: [UserService],
})
export class UsersModule {}

// posts.module.ts — imports SharedModule, not UsersModule
@Module({
  imports: [SharedModule],
  providers: [PostService],
  exports: [PostService],
})
export class PostsModule {}

Move shared logic to a base service:

// Before — UserService calls PostService, PostService calls UserService
// After — both call a database service directly without importing each other

// database/database.service.ts
@Injectable()
export class DatabaseService {
  async findUserById(id: number): Promise<User> {
    return this.userRepository.findOne({ where: { id } });
  }

  async findPostsByAuthor(authorId: number): Promise<Post[]> {
    return this.postRepository.find({ where: { authorId } });
  }
}

Use events to decouple services:

// Instead of PostService calling UserService directly,
// emit an event that UserService listens to
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class PostService {
  constructor(private eventEmitter: EventEmitter2) {}

  async createPost(data: CreatePostDto) {
    const post = await this.postRepository.save(data);
    // Emit event instead of calling UserService directly
    this.eventEmitter.emit('post.created', { postId: post.id, userId: data.authorId });
    return post;
  }
}

@Injectable()
export class UserService {
  @OnEvent('post.created')
  async handlePostCreated(event: { postId: number; userId: number }) {
    await this.updateUserPostCount(event.userId);
  }
}

Circular Dependencies Across DI Frameworks

NestJS is not the only framework where circular dependencies cause startup failures. Understanding how other DI containers handle the same problem helps you recognize the pattern and apply the right fix regardless of the framework.

Angular (TypeScript, Frontend)

Angular’s dependency injection system is architecturally similar to NestJS (NestJS was inspired by Angular). Angular handles circular dependencies almost identically: you use forwardRef() on both sides of the cycle. The difference is that Angular’s forwardRef() lives in @angular/core rather than @nestjs/common, and Angular additionally has a providedIn: 'root' pattern that registers services globally, which can sometimes avoid module-level cycles entirely because the service is not tied to a specific module’s import chain.

// Angular — same forwardRef pattern
@Injectable()
export class ServiceA {
  constructor(@Inject(forwardRef(() => ServiceB)) private b: ServiceB) {}
}

Spring Boot (Java)

Spring’s IoC container historically allowed circular dependencies by default. Spring would create a partially initialized bean, inject the reference into the dependent bean, then finish initialization. This worked silently for constructor injection (with a warning) and reliably for setter injection. Starting in Spring Boot 2.6, circular dependencies throw an error by default. You can re-enable the old behavior with spring.main.allow-circular-references=true, but Spring’s documentation explicitly recommends refactoring instead.

Spring’s approach — allowing partial construction — is arguably more dangerous than NestJS’s strict failure because it can mask design problems. A partially constructed bean that is accessed before initialization completes can cause null pointer exceptions that are difficult to trace.

// Spring — use @Lazy to break the cycle (similar to forwardRef)
@Service
public class UserService {
    private final PostService postService;

    public UserService(@Lazy PostService postService) {
        this.postService = postService;
    }
}

InversifyJS (TypeScript)

InversifyJS is a standalone DI container often used in Node.js projects without a framework. It does not support circular dependencies at all — there is no forwardRef() equivalent. If you have a cycle, you must refactor. InversifyJS provides lazyInject from inversify-inject-decorators as a workaround, which defers resolution to property access time, but this is a community extension, not a core feature.

// InversifyJS — lazyInject as a workaround
import { lazyInject } from 'inversify-inject-decorators';

class UserService {
  @lazyInject(TYPES.PostService)
  private postService!: PostService;
}

Module-Level vs Provider-Level Cycles

NestJS distinguishes between module-level and provider-level circular dependencies, and the fix differs:

Provider-level cycle: Two services in the same module (or across modules) inject each other. Fix with forwardRef() on the @Inject() decorator, or refactor to extract shared logic.

Module-level cycle: Two modules appear in each other’s imports array. Fix with forwardRef() on both imports entries, or restructure the module tree. Module-level cycles are more concerning because they indicate that two modules are not truly independent — they should likely be merged or have their shared dependency extracted.

Rule of thumb: If two modules form a cycle, ask “could these be one module?” If yes, merge them. If no, extract the shared dependency into a third module.

Fix 4: Detect Circular Dependencies Early

NestJS provides a built-in dependency graph visualization. Use it during development to spot cycles before they cause runtime errors:

# Install NestJS CLI
npm install -g @nestjs/cli

# Generate a dependency graph (outputs a JSON or DOT graph)
nest info

# For visual graph — use NestJS DevTools
npm install @nestjs/devtools-integration
// main.ts — enable DevTools in development
import { NestFactory } from '@nestjs/core';
import { DevtoolsModule } from '@nestjs/devtools-integration';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    snapshot: true,  // Required for DevTools
  });
  await app.listen(3000);
}
bootstrap();

Simple manual cycle detection — trace the dependency chain:

UserService -> PostService -> CommentService -> UserService  <- cycle detected

Draw out your service dependencies. Any graph with a path from a node back to itself is a cycle.

Use eslint-plugin-import to detect circular imports at the TypeScript level:

npm install --save-dev eslint-plugin-import
// .eslintrc.json
{
  "plugins": ["import"],
  "rules": {
    "import/no-cycle": ["error", { "maxDepth": 3 }]
  }
}

Fix 5: Lazy-Load Services with ModuleRef

For cases where you only need a service in specific methods (not in the constructor), use ModuleRef to resolve it lazily:

import { Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { PostService } from '../posts/post.service';

@Injectable()
export class UserService implements OnModuleInit {
  private postService: PostService;

  constructor(private moduleRef: ModuleRef) {}

  onModuleInit() {
    // Resolve the service after all modules are initialized
    this.postService = this.moduleRef.get(PostService, { strict: false });
  }

  async getUserWithPosts(userId: number) {
    const posts = await this.postService.findByAuthor(userId);
    return { userId, posts };
  }
}

ModuleRef.get() with strict: false searches the entire application context, not just the current module scope. Without this flag, it only looks in the current module.

For request-scoped providers, use resolve() instead of get():

async someMethod() {
  const postService = await this.moduleRef.resolve(PostService);
  return postService.findAll();
}

Fix 6: Lazy Loading Modules

NestJS supports lazy loading entire modules, which can break circular dependencies by deferring module initialization:

import { Injectable } from '@nestjs/common';
import { LazyModuleLoader } from '@nestjs/core';

@Injectable()
export class UserService {
  constructor(private lazyModuleLoader: LazyModuleLoader) {}

  async getUserPosts(userId: number) {
    // Load PostsModule only when this method is called
    const { PostsModule } = await import('../posts/posts.module');
    const moduleRef = await this.lazyModuleLoader.load(() => PostsModule);

    const postService = moduleRef.get(PostService);
    return postService.findByAuthor(userId);
  }
}

Lazy loading is useful for large applications where some modules are rarely used. It avoids the circular dependency because the module is not in the imports array — it is loaded on demand at runtime. The trade-off is that the first call to getUserPosts has a cold-start delay while the module initializes.

Fix 7: Structure Modules to Avoid Cycles

A module hierarchy that naturally avoids cycles:

AppModule
  CoreModule (no deps on feature modules)
    DatabaseModule
    LoggerModule
    ConfigModule
  SharedModule (depends on CoreModule only)
    EmailService
    NotificationService
    AuditService
  UsersModule (depends on Core + Shared)
    UserService
  PostsModule (depends on Core + Shared + Users)
    PostService

Rules that prevent circular dependencies:

  1. Core modules don’t import feature modules.
  2. Shared modules only import core modules.
  3. Feature modules import core and shared — never each other directly.
  4. If two feature modules need to communicate, use events (EventEmitter2) or extract the shared logic into SharedModule.

Barrel exports from module boundaries:

// users/index.ts — public API of the UsersModule
export { UserService } from './user.service';
export { User } from './entities/user.entity';
export { CreateUserDto } from './dto/create-user.dto';

// Other modules import from the barrel, not internal files
// This makes dependencies explicit and easier to audit
import { UserService } from '../users';  // Clean boundary

Still Not Working?

Indirect cycles are harder to spot. If A depends on B, B depends on C, and C depends on A, adding forwardRef() to only A and C won’t help — you need to trace the full cycle and add forwardRef() to every link.

Check for accidental self-injection — a service that injects itself:

@Injectable()
export class UserService {
  constructor(
    private userService: UserService,  // Self-injection: always a circular dep
  ) {}
}

forwardRef() doesn’t work with all provider types. It works with class providers but may behave unexpectedly with factory providers. Prefer extracting shared logic over using forwardRef() with factories.

Verify the exported providers in each module. A circular import is harmless if neither module actually injects from the other — but the error appears if the import creates a circular module reference even without injecting providers. Use exports arrays to make only necessary providers available:

@Module({
  providers: [UserService, UserRepository],  // UserRepository is internal
  exports: [UserService],                    // Only UserService is exported
})
export class UsersModule {}

Check for TypeScript barrel file cycles. Even without a DI-level cycle, circular imports through index.ts barrel files can cause undefined at import time. If a barrel file re-exports from a module that imports from another barrel file that re-exports from the first, the JavaScript module loader resolves one of them as undefined. This manifests as “Cannot read properties of undefined” rather than a NestJS circular dependency error. The fix is to import directly from the source file instead of through the barrel:

// Instead of importing from barrel (may cause undefined)
import { UserService } from '../users';

// Import from the source file directly
import { UserService } from '../users/user.service';

Use madge to visualize all import cycles in the project:

npx madge --circular --extensions ts src/
# Lists all circular import chains in the TypeScript codebase

For related NestJS issues, see Fix: NestJS Module Not Found, Fix: NestJS Guard Not Working, Fix: Spring Circular Dependency Error, and Fix: Python Circular Import.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles