Break the Loop With forwardRef
Circular Dependencies
Two classes that need each other can't both be injected normally. Nest ships an escape hatch — forwardRef — for when refactoring isn't an option.
What you'll learn
- Identify a circular-dependency error in startup logs
- Use forwardRef in both directions between providers
- Prefer refactoring to a shared third module when possible
If A needs B in its constructor and B needs A in its constructor,
neither can be built first. Nest detects this and errors at startup. The
official workaround is forwardRef.
What the Error Looks Like
You’ll see something like:
Nest can't resolve dependencies of the UsersService (?).
Please make sure that the argument dependency at index [0] is
available in the UsersModule context. When the dependency at the question mark would resolve to a class that imports back to you, you’ve got a cycle.
Fix With forwardRef
Wrap both sides with forwardRef. The wrapper defers reading the
reference until it’s needed, so the import cycle stops mattering.
// users.service.ts
@Injectable()
export class UsersService {
constructor(
@Inject(forwardRef(() => OrdersService))
private readonly orders: OrdersService,
) {}
}
// orders.service.ts
@Injectable()
export class OrdersService {
constructor(
@Inject(forwardRef(() => UsersService))
private readonly users: UsersService,
) {}
} Modules Cycle Too
The same trick works at the module level when two feature modules import each other.
@Module({
imports: [forwardRef(() => OrdersModule)],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
@Module({
imports: [forwardRef(() => UsersModule)],
providers: [OrdersService],
exports: [OrdersService],
})
export class OrdersModule {} Prefer a Refactor
forwardRef works, but a cycle is usually a design smell. Three common
fixes that get rid of it instead:
- Extract a shared module. If
UsersServiceandOrdersServiceboth needBillingHelpers, pull that helper into a separate module both import. - Push events through a bus. Replace
users.notifyOrders()witheventEmitter.emit('user.created', ...)and let orders listen. Now the dependency only goes one way. - Invert the relationship. Often only one side really needs the other; the back-edge was added “just in case.”
// Better: one-way + an event
@Injectable()
export class UsersService {
constructor(private readonly events: EventEmitter2) {}
async create(dto: CreateUserDto) {
const user = await this.repo.save(dto);
this.events.emit('user.created', user); // orders can listen
return user;
}
} Use forwardRef when you must — and treat each instance as a TODO to
revisit when there’s time.