Circular dependencies cause modules to receive undefined at initialization time because JavaScript resolves the cycle by partially evaluating one module before the other is ready. This produces runtime bugs that are invisible to the type system and nearly impossible to reproduce reliably. CWE-1120 (Excessive Complexity) applies: circular graphs make the build tool's evaluation order non-deterministic and prevent tree-shaking, silently bloating your bundle. AI-generated codebases are especially prone to cycles because utility files get written to "reach back up" into components they originally served.
Critical because circular imports can produce silent `undefined` values at runtime that bypass type checking and are extremely difficult to trace back to their root cause.
Find all cycles first, then break them by moving shared logic to a file that neither participant imports. Run:
npx madge --circular src/
# or
npx dpdm --circular src/
To break a cycle: extract the shared code into a new neutral module, or use dependency injection. Prevent future cycles by adding eslint-plugin-import with the import/no-cycle rule — it fails the lint check the moment a cycle is introduced.
ID: code-maintainability.code-organization.no-circular-deps
Severity: critical
What to look for: Scan import statements across source files for cycles. A circular dependency exists when Module A imports from Module B, which imports from Module C (or directly back to A), creating a loop. Look for these common patterns:
utils file that imports from a componenttypes file that imports runtime valuesindex.ts) that re-export and create unintended cyclesFocus on src/, app/, components/, lib/, hooks/ directories. If the project has a tool like madge, dpdm, or eslint-plugin-import with the no-cycle rule configured, note its presence. Otherwise, trace imports manually for the 10-20 largest/most central files.
Pass criteria: Enumerate all import chains among the 10-20 most central files. No circular import chains detected in the source files examined. Barrel files do not create cycles. Report even on pass: "Examined X files, 0 circular import chains found."
Fail criteria: One or more circular import chains are found in source files. Do not pass when a barrel file (index.ts) re-exports create an indirect cycle — trace through barrel re-exports.
Skip (N/A) when: The project has only 1-3 source files with no meaningful import graph. Signal: fewer than 5 JavaScript/TypeScript source files.
Detail on fail: Name the specific cycle found. Example: "Circular dependency: components/UserCard.tsx → lib/auth.ts → components/UserCard.tsx" or "Barrel file src/components/index.ts creates cycle with components/Dashboard/index.ts"
Remediation: Circular dependencies can cause subtle runtime bugs (undefined values at initialization time), slow build tools, and make refactoring unpredictable. They're especially common in AI-generated codebases where utilities "reach back up" to components.
To find all cycles, run:
npx madge --circular src/
# or
npx dpdm --circular src/
To fix a cycle, identify which import is creating the loop and break it by:
Prevent future cycles by adding eslint-plugin-import with the import/no-cycle rule to your ESLint config.