MeWrite Docs

Circular dependency detected

モジュール間で循環依存が検出された場合のエラー

概要

モジュールA→モジュールB→モジュールAのように、モジュール間で循環的なインポート関係が発生した場合のエラーです。バンドラーや実行時に問題を引き起こします。

エラーメッセージ

Circular dependency detected:
  src/moduleA.js -> src/moduleB.js -> src/moduleA.js

Warning: Circular dependency: src/utils/index.js -> src/utils/helper.js -> src/utils/index.js

RangeError: Maximum call stack size exceeded
TypeError: Cannot read property 'xxx' of undefined

原因

  1. 相互依存: 2つのモジュールが互いにインポート
  2. 間接的な循環: A→B→C→Aのような間接的な依存
  3. バレルファイル: index.jsからの再エクスポートによる循環
  4. 設計上の問題: 責務の分離ができていない

解決策

1. 依存関係を可視化

1
2
3
4
5
6
7
8
# madgeで循環依存を検出
npm install -D madge

# 循環依存を検出
npx madge --circular src/

# グラフを生成
npx madge --image graph.svg src/

2. 共通モジュールを抽出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Bad: 循環依存
// userService.js
import { validateEmail } from './validationService';
export const createUser = (email) => { ... };

// validationService.js
import { createUser } from './userService';  // 循環!
export const validateEmail = (email) => { ... };

// Good: 共通部分を抽出
// validators.js(共通モジュール)
export const validateEmail = (email) => { ... };

// userService.js
import { validateEmail } from './validators';
export const createUser = (email) => { ... };

// validationService.js
import { validateEmail } from './validators';
export const validateUser = (user) => { ... };

3. インターフェース/型を分離

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Bad: 循環依存
// user.ts
import { Order } from './order';
export interface User {
  orders: Order[];
}

// order.ts
import { User } from './user';  // 循環!
export interface Order {
  user: User;
}

// Good: 型を別ファイルに
// types.ts
export interface User {
  id: string;
  name: string;
}

export interface Order {
  id: string;
  userId: string;
}

// user.ts
import { User, Order } from './types';
// ...

// order.ts
import { User, Order } from './types';
// ...

4. 遅延インポート(動的インポート)

1
2
3
4
5
6
7
8
// Bad: 静的インポートで循環
import { something } from './other';

// Good: 必要な時に動的インポート
export async function doSomething() {
  const { something } = await import('./other');
  return something();
}

5. 依存性注入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Bad: 直接依存
class UserService {
  constructor() {
    this.emailService = new EmailService();  // 循環の原因
  }
}

// Good: 依存性注入
class UserService {
  constructor(private emailService: EmailService) {}
}

// 外部で組み立て
const emailService = new EmailService();
const userService = new UserService(emailService);

6. バレルファイルの注意点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Bad: index.jsを経由した循環
// utils/index.js
export * from './helper';
export * from './formatter';

// utils/helper.js
import { format } from './index';  // 循環!

// Good: 直接インポート
// utils/helper.js
import { format } from './formatter';  // 直接参照

7. 層の分離(レイヤードアーキテクチャ)

// 正しい依存方向
Controllers → Services → Repositories → Models

// 上位層から下位層へのみ依存
// 下位層は上位層を知らない
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// models/user.ts
export interface User { ... }

// repositories/userRepository.ts
import { User } from '../models/user';
export class UserRepository { ... }

// services/userService.ts
import { UserRepository } from '../repositories/userRepository';
export class UserService { ... }

// controllers/userController.ts
import { UserService } from '../services/userService';
export class UserController { ... }

8. イベントバスパターン

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Bad: 直接的な相互依存
// orderService が userService を呼び、
// userService が orderService を呼ぶ

// Good: イベントバスで疎結合化
// eventBus.ts
import { EventEmitter } from 'events';
export const eventBus = new EventEmitter();

// userService.ts
import { eventBus } from './eventBus';
eventBus.on('orderCreated', (order) => {
  // ユーザーの注文履歴を更新
});

// orderService.ts
import { eventBus } from './eventBus';
export function createOrder(order) {
  // 注文作成処理
  eventBus.emit('orderCreated', order);
}

9. NestJSでのforwardRef

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// NestJSの場合
import { forwardRef, Inject } from '@nestjs/common';

@Injectable()
export class UserService {
  constructor(
    @Inject(forwardRef(() => OrderService))
    private orderService: OrderService,
  ) {}
}

@Injectable()
export class OrderService {
  constructor(
    @Inject(forwardRef(() => UserService))
    private userService: UserService,
  ) {}
}

10. ESLintで検出

1
2
3
4
5
6
7
// .eslintrc.js
module.exports = {
  plugins: ['import'],
  rules: {
    'import/no-cycle': ['error', { maxDepth: 10 }]
  }
};

11. Webpackで検出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// webpack.config.js
const CircularDependencyPlugin = require('circular-dependency-plugin');

module.exports = {
  plugins: [
    new CircularDependencyPlugin({
      exclude: /node_modules/,
      failOnError: true,
    })
  ]
};

設計原則

原則説明
単一責任原則1モジュール = 1責務
依存性逆転原則抽象に依存、具象に依存しない
層の分離上位→下位の一方向依存

よくある間違い

  • 便利だからとindex.jsから全てエクスポート
  • ユーティリティ関数を1つのファイルにまとめすぎる
  • 双方向の参照が必要な設計をしてしまう
  • 警告を無視して開発を続ける

関連エラー

参考リンク

JavaScript の他のエラー

最終更新: 2025-12-13