NestJS の Service における forwardRef の罠

NestJS で Service の循環参照によるエラーを解消するために `forwardRef` を用いると、なぜか**コンストラクターで開始した非同期処理による Service オブジェクトのメンバ変数の変更が効かなくなる**という問題が発生した。
今回は、この問題を再現するコードと、問題への対処法を紹介する。

# 問題の再現

循環参照する2個のサービス `HogeService` および `FugaService` を用意し、これらを `AppController` から参照する。

![構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/50b0a0ba-c9b6-4f40-a276-82605c1aa863.png)

```typescript:app.controller.ts
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](https://github.com/mikecat/nest-forwardref-test/tree/ac99540f2b49403818747c4631018a9f6074b0f1)

```typescript:hoge.service.ts
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;
  }
}
```

```typescript:fuga.service.ts
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;
  }
}
```

これを

```bash
npx nest start
```

コマンドで実行すると、以下のログが出た。

```text
[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!
```

さらに、

```bash
curl http://localhost:3000/
```

コマンドでデータを取得する。
結果を整形すると、以下のようになった。

```json
{
  "hoge": {
    "hoge": "hoge",
    "fuga": "got: fuga",
    "status": "ready"
  },
  "fuga": {
    "hoge": "got: ",
    "fuga": "fuga",
    "status": "ready"
  }
}
```

## 循環参照をするコード (エラー)

コメントアウトを解除し、循環参照をする。

[mikecat/nest-forwardref-test at 0d48849b22a715e5982d6d207860922b879a6686](https://github.com/mikecat/nest-forwardref-test/tree/0d48849b22a715e5982d6d207860922b879a6686)

```typescript:fuga.service.ts
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;
  }
}
```

実行すると、以下のエラーが出た。

```text
[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](https://docs.nestjs.com/fundamentals/circular-dependency)
に沿って `forwardRef` を導入し、エラーを解消する。

[mikecat/nest-forwardref-test at 0dcfb374f0c9d7ee3ccfad60e00d64cdc92e3e42](https://github.com/mikecat/nest-forwardref-test/tree/0dcfb374f0c9d7ee3ccfad60e00d64cdc92e3e42)

```typescript:hoge.service.ts
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` も同様に実装する。

実行すると、以下のログが出力された。

```text
[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!` が出力されている

ことを確認しておこう。

```bash
curl http://localhost:3000/
```

コマンドで取得した情報を整形すると、以下のようになった。

```json
{
  "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](https://github.com/mikecat/nest-forwardref-test/tree/c1db9e0627008627b585b949f75e3fa947c41118)

```typescript:hoge.service.ts
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` も同様に実装する。

実行すると、以下のログが出力された。

```text
[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!
```

さらに、

```bash
curl http://localhost:3000/
```

コマンドで取得した情報を整形すると、以下のようになった。

```json
{
  "hoge": {
    "hoge": "hoge",
    "fuga": "got: fuga",
    "status": "ready"
  },
  "fuga": {
    "hoge": "got: hoge",
    "fuga": "fuga",
    "status": "ready"
  }
}
```

期待通り `status` が `ready` になっている。

## ModuleRef を用いる

[Circular Dependency](https://docs.nestjs.com/fundamentals/circular-dependency) のページをよく見ると、`ModuleRef` を使ってもエラーを回避できるということが書かれている。
そこで、これを用いてみる。
[Module reference | NestJS - A progressive Node.js framework](https://docs.nestjs.com/fundamentals/module-ref)

[mikecat/nest-forwardref-test at b6406449822a9a9ec849eda03809d6b1ebd2b450](https://github.com/mikecat/nest-forwardref-test/tree/b6406449822a9a9ec849eda03809d6b1ebd2b450)

```typescript:hoge.service.ts
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` も同様に実装する。

実行すると、以下のログが出力された。

```text
[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!
```

さらに、

```bash
curl http://localhost:3000/
```

コマンドで取得した情報を整形すると、以下のようになった。

```json
{
  "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` を用いて循環参照によるエラーを解消すると、メンバ変数の変更が効くようになり、状態の管理用の新たなオブジェクトを導入することなく問題を回避できた。