0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NestJS の Service における forwardRef の罠

Posted at

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

問題の再現

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

構成

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

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;
  }
}
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;
  }
}

これを

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

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;
  }
}

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

[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

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 も同様に実装する。

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

[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"
  }
}

なぜか、取得した statusloading になっている。
この取得はログに load completed! が出力された後で行っており、最初の「循環参照をしないコード」と同様に ready が出力されることが期待される。
これは妙である。

問題への対処法

状態をオブジェクトに格納する

メンバ変数に直接格納した値はなぜか更新されないようだが、メンバ変数に格納したオブジェクトのプロパティは更新し、それを反映させることができた。

mikecat/nest-forwardref-test at c1db9e0627008627b585b949f75e3fa947c41118

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 も同様に実装する。

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

[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"
  }
}

期待通り statusready になっている。

ModuleRef を用いる

Circular Dependency のページをよく見ると、ModuleRef を使ってもエラーを回避できるということが書かれている。
そこで、これを用いてみる。
Module reference | NestJS - A progressive Node.js framework

mikecat/nest-forwardref-test at b6406449822a9a9ec849eda03809d6b1ebd2b450

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 も同様に実装する。

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

[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"
  }
}

期待通り statusready になっている。
余計なオブジェクトを導入しないなど、「循環参照をするコード (エラー)」からのコードの変更幅が小さく、「状態をオブジェクトに格納する」対策よりも良いと考えられる。

まとめ

NestJS における Service の循環参照によるエラーを forwardRef を用いて解消すると、なぜかコンストラクターで開始した非同期処理による Service オブジェクトのメンバ変数の変更が効かなくなるという問題が発生した。
メンバ変数を直接変更しても効かないものの、メンバ変数に格納したオブジェクトへの変更は効いたため、Service オブジェクトのシャローコピーのような現象が発生していると推測できる。

forwardRef を用いず、ModuleRef を用いて循環参照によるエラーを解消すると、メンバ変数の変更が効くようになり、状態の管理用の新たなオブジェクトを導入することなく問題を回避できた。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?