How to Resolve Circular Dependencies in NestJS: Complete Guide to forwardRef()

Use NestJS's forwardRef() utility to wrap imports and injections on both sides of a bidirectional relationship, deferring class resolution until the dependency injection container completes the module graph.

Circular dependency resolution in NestJS is essential when two modules or providers must reference each other directly. Without proper handling, the synchronous nature of JavaScript module evaluation causes the second import to resolve as undefined, triggering runtime errors. The nestjs/nest source code implements a lazy-evaluation strategy through forward references that delays instantiation until after the scanner builds the complete dependency graph.

How Circular Dependencies Break at Runtime

When Module A imports Module B, and Module B imports Module A, the JavaScript module loader evaluates whichever file is imported first. The second file evaluates to undefined because its dependencies haven't finished loading yet. NestJS detects this scenario during the scanning phase and throws a CircularDependencyException from packages/core/errors/exceptions/circular-dependency.exception.ts, prompting developers to use forwardRef():

export class CircularDependencyException extends RuntimeException {
  constructor(context?: string) {
    const ctx = context ? ` inside ${context}` : ``;
    super(
      `A circular dependency has been detected${ctx}. Please, make sure that each side of a bidirectional relationships are decorated with "forwardRef()".`,
    );
  }
}

The forwardRef() Mechanism in the NestJS Core

NestJS solves circular dependency resolution through a deferred reference pattern implemented across the common utilities and core scanner.

Creating Forward References with forwardRef()

The forwardRef() utility in packages/common/utils/forward-ref.util.ts accepts a callback that returns a class and wraps it in an object implementing the ForwardReference interface:

export const forwardRef = (fn: () => any): ForwardReference => ({
  forwardRef: fn,
});

This wrapper prevents immediate evaluation of the class constructor, allowing the module system to finish loading all files before resolving the actual dependency.

Scanner Detection and Resolution

The module scanner in packages/core/scanner.ts detects forward references using the isForwardReference() type guard:

private isForwardReference(module: ModuleDefinition): module is ForwardReference {
  return module && !!(module as ForwardReference).forwardRef;
}

When the scanner encounters a forward reference during scanForModules, it invokes the stored callback to retrieve the real class:

if (this.isForwardReference(moduleDefinition)) {
  moduleDefinition = (moduleDefinition as ForwardReference).forwardRef();
}

The scanner also handles forward references when inserting imports via insertImport():

if (this.isForwardReference(related)) {
  return this.container.addImport(related.forwardRef(), token);
}

This lazy resolution ensures that both modules exist in the container's registry before Nest attempts to wire their dependencies.

Implementing Circular Dependency Resolution in Practice

Apply forwardRef() to both sides of any bidirectional relationship—whether between modules or between individual providers.

Resolving Module-to-Module Circular Imports

When two modules depend on each other's services, wrap the import in forwardRef() within each module's @Module() decorator. The integration tests in integration/testing-module-override/circular-dependency/ demonstrate this pattern:

// a.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { BModule } from './b.module';

@Module({
  imports: [forwardRef(() => BModule)],
  providers: [AService],
  exports: [AService],
})
export class AModule {}
// b.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { AModule } from './a.module';

@Module({
  imports: [forwardRef(() => AModule)],
  providers: [BService],
  exports: [BService],
})
export class BModule {}

Both modules import each other using forwardRef(() => OtherModule). The scanner executes these callbacks only after collecting all module definitions, breaking the circularity safely.

Resolving Provider-to-Provider Circular Injection

For circular dependencies between individual providers within the same or different modules, use @Inject() combined with forwardRef() in the constructor:

// circular.service.ts
import { Injectable, forwardRef, Inject } from '@nestjs/common';
import { InputService } from './input.service';

@Injectable()
export class CircularService {
  constructor(
    @Inject(forwardRef(() => InputService))
    private readonly input: InputService,
  ) {}
}
// input.service.ts
import { Injectable, forwardRef, Inject } from '@nestjs/common';
import { CircularService } from './circular.service';

@Injectable()
export class InputService {
  constructor(
    @Inject(forwardRef(() => CircularService))
    private readonly circular: CircularService,
  ) {}
}

Each provider injects the other through @Inject(forwardRef(() => TargetService)). Nest resolves these callbacks after instantiating the full provider graph, located in examples such as integration/inspector/src/circular-modules/.

Summary

  • Forward references defer evaluation: The forwardRef() utility in packages/common/utils/forward-ref.util.ts wraps classes in callbacks that execute after the module graph is complete.
  • Scanner integration: packages/core/scanner.ts detects forward references via isForwardReference() and resolves them during module scanning and import insertion.
  • Module circularity: Use forwardRef(() => OtherModule) in the imports array of both modules' @Module() decorators.
  • Provider circularity: Use @Inject(forwardRef(() => OtherService)) in both providers' constructors to break the instantiation deadlock.
  • Error guidance: If you omit forwardRef() on either side, Nest throws a CircularDependencyException from packages/core/errors/exceptions/circular-dependency.exception.ts identifying the problematic context.

Frequently Asked Questions

What causes circular dependency errors in NestJS?

Circular dependency errors occur when two modules or providers import each other directly without lazy resolution. JavaScript's synchronous module system evaluates the first imported file completely before the second, causing the second import to be undefined when the first file references it. NestJS detects this undefined resolution and throws a CircularDependencyException to prevent runtime injection failures.

When should I use forwardRef() versus restructuring my code?

Use forwardRef() when the bidirectional relationship is architecturally necessary—such as when Module A exposes services that Module B extends, and Module B provides utilities that Module A consumes. If the circularity indicates a design flaw where both modules share too many concerns, refactor by extracting common logic into a third shared module instead of using forward references.

Can I use forwardRef() with custom providers?

Yes. Apply forwardRef() within the @Module() providers array when using custom provider syntax such as useFactory, useClass, or useValue. For factory providers that depend on circular services, wrap the injected token in forwardRef(() => CircularService) within the factory's inject array or constructor injection parameters.

How does NestJS detect circular dependencies at runtime?

The scanner in packages/core/scanner.ts validates module references during the bootstrap phase. If a forward reference callback returns undefined when invoked, or if the container encounters an unresolvable token during provider instantiation, Nest throws a CircularDependencyException. This validates that developers have explicitly marked both sides of circular relationships with forwardRef(), ensuring intentional rather than accidental circular architecture.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →