Fix: NestJS Circular Dependency — forwardRef and Module Design
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 -> UsersModuleOr NestJS silently creates providers as undefined:
TypeError: Cannot read properties of undefined (reading 'findUser')
// userService is undefined because of unresolved circular depWhy 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 dependency —
UserServiceinjectsPostService, andPostServicealso injectsUserService. NestJS can’t instantiate either because each requires the other to exist first. - Module circular dependency —
UsersModuleimportsPostsModule, andPostsModuleimportsUsersModule. Same deadlock at the module level. - Indirect circular dependency —
AtoBtoCtoA. 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 needforwardRef()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 useforwardRef(). 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 detectedDraw 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)
PostServiceRules that prevent circular dependencies:
- Core modules don’t import feature modules.
- Shared modules only import core modules.
- Feature modules import core and shared — never each other directly.
- If two feature modules need to communicate, use events (
EventEmitter2) or extract the shared logic intoSharedModule.
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 boundaryStill 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 codebaseFor related NestJS issues, see Fix: NestJS Module Not Found, Fix: NestJS Guard Not Working, Fix: Spring Circular Dependency Error, and Fix: Python Circular Import.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: NestJS Swagger UI Not Showing — /api-docs Returns 404 or Blank Page
How to fix NestJS Swagger UI not displaying — SwaggerModule setup, DocumentBuilder, decorators not appearing, guards blocking the docs route, and Fastify vs Express differences.
Fix: NestJS Nest can't resolve dependencies — Provider Not Found Error
How to fix NestJS dependency injection errors — module imports, provider exports, circular dependencies, dynamic modules, and the most common 'can't resolve dependencies' patterns.
Fix: NestJS ValidationPipe Not Working — class-validator Decorators Ignored
How to fix NestJS ValidationPipe not validating requests — global pipe setup, class-transformer, whitelist and transform options, custom validators, and DTO inheritance issues.
Fix: NestJS Guard Not Working — canActivate Always Passes or Is Never Called
How to fix NestJS guards not working — applying guards globally vs controller vs method level, JWT AuthGuard, metadata with Reflector, public routes, and guard execution order.