NestJS で Service の循環参照によるエラーを解消するために forwardRef
を用いると、なぜかコンストラクターで開始した非同期処理による Service オブジェクトのメンバ変数の変更が効かなくなるという問題が発生した。
今回は、この問題を再現するコードと、問題への対処法を紹介する。
問題の再現
循環参照する2個のサービス HogeService
および FugaService
を用意し、これらを AppController
から参照する。
import { Controller, Get } from '@nestjs/common';
import { HogeService } from './hoge.service';
import { FugaService } from './fuga.service';
@Controller()
export class AppController {
constructor(
private readonly hogeService: HogeService,
private readonly fugaService: FugaService
) {}
@Get()
getData() {
return {
hoge: {
hoge: this.hogeService.getHoge(),
fuga: this.hogeService.getFugaFromFugaService(),
status: this.hogeService.getStatus(),
},
fuga: {
hoge: this.fugaService.getHogeFromHogeService(),
fuga: this.fugaService.getFuga(),
status: this.fugaService.getStatus(),
},
};
}
}
循環参照をしないコード
まずは、循環参照をするコードの一部をコメントアウトし、循環参照をしない状態で動作確認を行う。
mikecat/nest-forwardref-test at ac99540f2b49403818747c4631018a9f6074b0f1
import { Injectable, Logger } from '@nestjs/common';
import { FugaService } from './fuga.service';
@Injectable()
export class HogeService {
private readonly logger: Logger = new Logger('HogeService');
private status: string = 'loading';
constructor(private readonly fugaService: FugaService) {
this.logger.log('loading...');
setTimeout(() => {
this.status = 'ready';
this.logger.log('load completed!');
}, 1000);
}
getHoge(): string {
return 'hoge';
}
getFugaFromFugaService(): string {
return 'got: ' + this.fugaService.getFuga();
}
getStatus(): string {
return this.status;
}
}
import { Injectable, Logger } from '@nestjs/common';
import { HogeService } from './hoge.service';
@Injectable()
export class FugaService {
private readonly logger: Logger = new Logger('FugaService');
private status: string = 'loading';
constructor(/*private readonly hogeService: HogeService*/) {
this.logger.log('loading...');
setTimeout(() => {
this.status = 'ready';
this.logger.log('load completed!');
}, 1000);
}
getFuga(): string {
return 'fuga';
}
getHogeFromHogeService(): string {
return 'got: ' /*+ this.hogeService.getHoge()*/;
}
getStatus(): string {
return this.status;
}
}
これを
npx nest start
コマンドで実行すると、以下のログが出た。
[Nest] 16800 - 2025/03/18 21:48:59 LOG [NestFactory] Starting Nest application...
[Nest] 16800 - 2025/03/18 21:48:59 LOG [FugaService] loading...
[Nest] 16800 - 2025/03/18 21:48:59 LOG [HogeService] loading...
[Nest] 16800 - 2025/03/18 21:48:59 LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 16800 - 2025/03/18 21:48:59 LOG [RoutesResolver] AppController {/}: +3ms
[Nest] 16800 - 2025/03/18 21:48:59 LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 16800 - 2025/03/18 21:48:59 LOG [NestApplication] Nest application successfully started +1ms
[Nest] 16800 - 2025/03/18 21:49:00 LOG [FugaService] load completed!
[Nest] 16800 - 2025/03/18 21:49:00 LOG [HogeService] load completed!
さらに、
curl http://localhost:3000/
コマンドでデータを取得する。
結果を整形すると、以下のようになった。
{
"hoge": {
"hoge": "hoge",
"fuga": "got: fuga",
"status": "ready"
},
"fuga": {
"hoge": "got: ",
"fuga": "fuga",
"status": "ready"
}
}
循環参照をするコード (エラー)
コメントアウトを解除し、循環参照をする。
mikecat/nest-forwardref-test at 0d48849b22a715e5982d6d207860922b879a6686
import { Injectable, Logger } from '@nestjs/common';
import { HogeService } from './hoge.service';
@Injectable()
export class FugaService {
private readonly logger: Logger = new Logger('FugaService');
private status: string = 'loading';
constructor(private readonly hogeService: HogeService) {
this.logger.log('loading...');
setTimeout(() => {
this.status = 'ready';
this.logger.log('load completed!');
}, 1000);
}
getFuga(): string {
return 'fuga';
}
getHogeFromHogeService(): string {
return 'got: ' + this.hogeService.getHoge();
}
getStatus(): string {
return this.status;
}
}
実行すると、以下のエラーが出た。
[Nest] 13784 - 2025/03/18 21:50:45 LOG [NestFactory] Starting Nest application...
[Nest] 13784 - 2025/03/18 21:50:45 LOG [InjectorLogger] Nest encountered an undefined dependency. This may be due to a circular import or a missing dependency declaration.
[Nest] 13784 - 2025/03/18 21:50:45 ERROR [ExceptionHandler] UndefinedDependencyException [Error]: Nest can't resolve dependencies of the FugaService (?). Please make sure that the argument dependency at index [0] is available in the AppModule context.
Potential solutions:
- Is AppModule a valid NestJS module?
- If dependency is a provider, is it part of the current AppModule?
- If dependency is exported from a separate @Module, is that module imported within AppModule?
@Module({
imports: [ /* the Module containing dependency */ ]
})
forwardRef を使うコード
Circular Dependency | NestJS - A progressive Node.js framework
に沿って forwardRef
を導入し、エラーを解消する。
mikecat/nest-forwardref-test at 0dcfb374f0c9d7ee3ccfad60e00d64cdc92e3e42
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { FugaService } from './fuga.service';
@Injectable()
export class HogeService {
private readonly logger: Logger = new Logger('HogeService');
private status: string = 'loading';
constructor(
@Inject(forwardRef(() => FugaService))
private readonly fugaService: FugaService,
) {
this.logger.log('loading...');
setTimeout(() => {
this.status = 'ready';
this.logger.log('load completed!');
}, 1000);
}
getHoge(): string {
return 'hoge';
}
getFugaFromFugaService(): string {
return 'got: ' + this.fugaService.getFuga();
}
getStatus(): string {
return this.status;
}
}
fuga.service.ts
も同様に実装する。
実行すると、以下のログが出力された。
[Nest] 15640 - 2025/03/18 21:59:09 LOG [NestFactory] Starting Nest application...
[Nest] 15640 - 2025/03/18 21:59:09 LOG [HogeService] loading...
[Nest] 15640 - 2025/03/18 21:59:09 LOG [FugaService] loading...
[Nest] 15640 - 2025/03/18 21:59:09 LOG [InstanceLoader] AppModule dependencies initialized +1ms
[Nest] 15640 - 2025/03/18 21:59:09 LOG [RoutesResolver] AppController {/}: +4ms
[Nest] 15640 - 2025/03/18 21:59:09 LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 15640 - 2025/03/18 21:59:09 LOG [NestApplication] Nest application successfully started +1ms
[Nest] 15640 - 2025/03/18 21:59:10 LOG [HogeService] load completed!
[Nest] 15640 - 2025/03/18 21:59:10 LOG [FugaService] load completed!
ここで、HogeService
および FugaService
それぞれについて、
-
loading...
がちょうど1度ずつ出力されている -
load completed!
が出力されている
ことを確認しておこう。
curl http://localhost:3000/
コマンドで取得した情報を整形すると、以下のようになった。
{
"hoge": {
"hoge": "hoge",
"fuga": "got: fuga",
"status": "loading"
},
"fuga": {
"hoge": "got: hoge",
"fuga": "fuga",
"status": "loading"
}
}
なぜか、取得した status
が loading
になっている。
この取得はログに load completed!
が出力された後で行っており、最初の「循環参照をしないコード」と同様に ready
が出力されることが期待される。
これは妙である。
問題への対処法
状態をオブジェクトに格納する
メンバ変数に直接格納した値はなぜか更新されないようだが、メンバ変数に格納したオブジェクトのプロパティは更新し、それを反映させることができた。
mikecat/nest-forwardref-test at c1db9e0627008627b585b949f75e3fa947c41118
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { FugaService } from './fuga.service';
@Injectable()
export class HogeService {
private readonly logger: Logger = new Logger('HogeService');
private readonly data: {
status: string;
} = {
status: 'loading',
};
constructor(
@Inject(forwardRef(() => FugaService))
private readonly fugaService: FugaService,
) {
this.logger.log('loading...');
setTimeout(() => {
this.data.status = 'ready';
this.logger.log('load completed!');
}, 1000);
}
getHoge(): string {
return 'hoge';
}
getFugaFromFugaService(): string {
return 'got: ' + this.fugaService.getFuga();
}
getStatus(): string {
return this.data.status;
}
}
fuga.service.ts
も同様に実装する。
実行すると、以下のログが出力された。
[Nest] 15576 - 2025/03/18 22:11:06 LOG [NestFactory] Starting Nest application...
[Nest] 15576 - 2025/03/18 22:11:06 LOG [HogeService] loading...
[Nest] 15576 - 2025/03/18 22:11:06 LOG [FugaService] loading...
[Nest] 15576 - 2025/03/18 22:11:06 LOG [InstanceLoader] AppModule dependencies initialized +1ms
[Nest] 15576 - 2025/03/18 22:11:06 LOG [RoutesResolver] AppController {/}: +2ms
[Nest] 15576 - 2025/03/18 22:11:06 LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 15576 - 2025/03/18 22:11:06 LOG [NestApplication] Nest application successfully started +1ms
[Nest] 15576 - 2025/03/18 22:11:07 LOG [HogeService] load completed!
[Nest] 15576 - 2025/03/18 22:11:07 LOG [FugaService] load completed!
さらに、
curl http://localhost:3000/
コマンドで取得した情報を整形すると、以下のようになった。
{
"hoge": {
"hoge": "hoge",
"fuga": "got: fuga",
"status": "ready"
},
"fuga": {
"hoge": "got: hoge",
"fuga": "fuga",
"status": "ready"
}
}
期待通り status
が ready
になっている。
ModuleRef を用いる
Circular Dependency のページをよく見ると、ModuleRef
を使ってもエラーを回避できるということが書かれている。
そこで、これを用いてみる。
Module reference | NestJS - A progressive Node.js framework
mikecat/nest-forwardref-test at b6406449822a9a9ec849eda03809d6b1ebd2b450
import { Injectable, Logger } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { FugaService } from './fuga.service';
@Injectable()
export class HogeService {
private readonly logger: Logger = new Logger('HogeService');
private readonly fugaService: FugaService;
private status: string = 'loading';
constructor(private moduleRef: ModuleRef) {
this.fugaService = this.moduleRef.get(FugaService);
this.logger.log('loading...');
setTimeout(() => {
this.status = 'ready';
this.logger.log('load completed!');
}, 1000);
}
getHoge(): string {
return 'hoge';
}
getFugaFromFugaService(): string {
return 'got: ' + this.fugaService.getFuga();
}
getStatus(): string {
return this.status;
}
}
fuga.service.ts
も同様に実装する。
実行すると、以下のログが出力された。
[Nest] 17572 - 2025/03/18 22:19:25 LOG [NestFactory] Starting Nest application...
[Nest] 17572 - 2025/03/18 22:19:25 LOG [HogeService] loading...
[Nest] 17572 - 2025/03/18 22:19:25 LOG [FugaService] loading...
[Nest] 17572 - 2025/03/18 22:19:25 LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 17572 - 2025/03/18 22:19:25 LOG [RoutesResolver] AppController {/}: +2ms
[Nest] 17572 - 2025/03/18 22:19:25 LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 17572 - 2025/03/18 22:19:25 LOG [NestApplication] Nest application successfully started +2ms
[Nest] 17572 - 2025/03/18 22:19:26 LOG [HogeService] load completed!
[Nest] 17572 - 2025/03/18 22:19:26 LOG [FugaService] load completed!
さらに、
curl http://localhost:3000/
コマンドで取得した情報を整形すると、以下のようになった。
{
"hoge": {
"hoge": "hoge",
"fuga": "got: fuga",
"status": "ready"
},
"fuga": {
"hoge": "got: hoge",
"fuga": "fuga",
"status": "ready"
}
}
期待通り status
が ready
になっている。
余計なオブジェクトを導入しないなど、「循環参照をするコード (エラー)」からのコードの変更幅が小さく、「状態をオブジェクトに格納する」対策よりも良いと考えられる。
まとめ
NestJS における Service の循環参照によるエラーを forwardRef
を用いて解消すると、なぜかコンストラクターで開始した非同期処理による Service オブジェクトのメンバ変数の変更が効かなくなるという問題が発生した。
メンバ変数を直接変更しても効かないものの、メンバ変数に格納したオブジェクトへの変更は効いたため、Service オブジェクトのシャローコピーのような現象が発生していると推測できる。
forwardRef
を用いず、ModuleRef
を用いて循環参照によるエラーを解消すると、メンバ変数の変更が効くようになり、状態の管理用の新たなオブジェクトを導入することなく問題を回避できた。